1use zeroize::Zeroizing;
4
5#[derive(Debug, Clone)]
7pub struct SecretKeyValue {
8 pub key: String,
10 pub value: Zeroizing<String>,
12}
13
14impl SecretKeyValue {
15 pub fn new(key: String, value: String) -> Result<Self, crate::ValidationError> {
17 crate::models::secret_data::validate_key_name(&key)?;
19
20 if value.is_empty() {
21 return Err(crate::ValidationError::Other(
22 "value cannot be empty".to_string(),
23 ));
24 }
25
26 Ok(Self {
27 key,
28 value: Zeroizing::new(value),
29 })
30 }
31}
32
33pub fn validate_secret_name(name: &str) -> Result<(), crate::ValidationError> {
38 if name.is_empty() || name.len() > 253 {
39 return Err(crate::ValidationError::InvalidSecretName {
40 name: name.to_string(),
41 reason: "must be 1-253 characters".to_string(),
42 });
43 }
44
45 if !name
47 .chars()
48 .next()
49 .map(|c| c.is_ascii_alphanumeric())
50 .unwrap_or(false)
51 {
52 return Err(crate::ValidationError::InvalidSecretName {
53 name: name.to_string(),
54 reason: "must start with alphanumeric character".to_string(),
55 });
56 }
57
58 if !name
60 .chars()
61 .last()
62 .map(|c| c.is_ascii_alphanumeric())
63 .unwrap_or(false)
64 {
65 return Err(crate::ValidationError::InvalidSecretName {
66 name: name.to_string(),
67 reason: "must end with alphanumeric character".to_string(),
68 });
69 }
70
71 if !name
73 .chars()
74 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.')
75 {
76 return Err(crate::ValidationError::InvalidSecretName {
77 name: name.to_string(),
78 reason: "must contain only lowercase alphanumeric, hyphens, or dots".to_string(),
79 });
80 }
81
82 Ok(())
83}
84
85pub fn validate_namespace(namespace: &str) -> Result<(), crate::ValidationError> {
90 if namespace.is_empty() || namespace.len() > 63 {
91 return Err(crate::ValidationError::InvalidNamespace {
92 namespace: namespace.to_string(),
93 reason: "must be 1-63 characters".to_string(),
94 });
95 }
96
97 if !namespace
99 .chars()
100 .next()
101 .map(|c| c.is_ascii_alphanumeric())
102 .unwrap_or(false)
103 {
104 return Err(crate::ValidationError::InvalidNamespace {
105 namespace: namespace.to_string(),
106 reason: "must start with alphanumeric character".to_string(),
107 });
108 }
109
110 if !namespace
112 .chars()
113 .last()
114 .map(|c| c.is_ascii_alphanumeric())
115 .unwrap_or(false)
116 {
117 return Err(crate::ValidationError::InvalidNamespace {
118 namespace: namespace.to_string(),
119 reason: "must end with alphanumeric character".to_string(),
120 });
121 }
122
123 if !namespace
125 .chars()
126 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
127 {
128 return Err(crate::ValidationError::InvalidNamespace {
129 namespace: namespace.to_string(),
130 reason: "must contain only lowercase alphanumeric or hyphens".to_string(),
131 });
132 }
133
134 Ok(())
135}
136
137pub fn validate_key_name(key: &str) -> Result<(), crate::ValidationError> {
141 if key.is_empty() {
142 return Err(crate::ValidationError::InvalidKeyName {
143 key: key.to_string(),
144 reason: "key name cannot be empty".to_string(),
145 });
146 }
147
148 let first_char = key.chars().next().unwrap();
150 if !first_char.is_ascii_alphabetic() && first_char != '_' {
151 return Err(crate::ValidationError::InvalidKeyName {
152 key: key.to_string(),
153 reason: "must start with letter or underscore".to_string(),
154 });
155 }
156
157 if !key
159 .chars()
160 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
161 {
162 return Err(crate::ValidationError::InvalidKeyName {
163 key: key.to_string(),
164 reason: "must contain only alphanumeric, underscores, or hyphens".to_string(),
165 });
166 }
167
168 Ok(())
169}
170
171use base64::Engine;
172use serde::{Deserialize, Serialize};
173use std::collections::HashMap;
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct SecretMetadata {
178 pub name: String,
179 pub namespace: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub labels: Option<HashMap<String, String>>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub annotations: Option<HashMap<String, String>>,
184}
185
186#[derive(Debug, Clone)]
188pub struct SecretRequest {
189 pub name: String,
190 pub namespace: String,
191 pub labels: HashMap<String, String>,
192 pub annotations: HashMap<String, String>,
193 pub data: Vec<SecretKeyValue>,
194}
195
196impl SecretRequest {
197 pub fn new(
199 name: String,
200 namespace: String,
201 labels: HashMap<String, String>,
202 annotations: HashMap<String, String>,
203 data: Vec<SecretKeyValue>,
204 ) -> Result<Self, crate::ValidationError> {
205 validate_secret_name(&name)?;
207 validate_namespace(&namespace)?;
208
209 if data.is_empty() {
211 return Err(crate::ValidationError::EmptySecretData);
212 }
213
214 if data.len() > 50 {
216 return Err(crate::ValidationError::TooManyKeyValuePairs {
217 count: data.len(),
218 max: 50,
219 });
220 }
221
222 Ok(Self {
223 name,
224 namespace,
225 labels,
226 annotations,
227 data,
228 })
229 }
230
231 pub fn metadata(&self) -> SecretMetadata {
233 SecretMetadata {
234 name: self.name.clone(),
235 namespace: self.namespace.clone(),
236 labels: if self.labels.is_empty() {
237 None
238 } else {
239 Some(self.labels.clone())
240 },
241 annotations: if self.annotations.is_empty() {
242 None
243 } else {
244 Some(self.annotations.clone())
245 },
246 }
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct SecretManifest {
253 #[serde(rename = "apiVersion")]
254 pub api_version: String,
255 pub kind: String,
256 pub metadata: SecretMetadata,
257 #[serde(rename = "type")]
258 pub secret_type: String,
259 pub data: HashMap<String, String>,
260}
261
262impl SecretManifest {
263 pub fn from_request(request: &SecretRequest) -> Self {
265 let mut data = HashMap::new();
266
267 for kv in &request.data {
269 let encoded = base64::engine::general_purpose::STANDARD.encode(kv.value.as_bytes());
270 data.insert(kv.key.clone(), encoded);
271 }
272
273 Self {
274 api_version: "v1".to_string(),
275 kind: "Secret".to_string(),
276 metadata: request.metadata(),
277 secret_type: "Opaque".to_string(),
278 data,
279 }
280 }
281
282 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
284 serde_yaml::to_string(self)
285 }
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct SealedSecretManifest {
291 #[serde(rename = "apiVersion")]
292 pub api_version: String,
293 pub kind: String,
294 pub metadata: SecretMetadata,
295 pub spec: SealedSecretSpec,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct SealedSecretSpec {
300 #[serde(rename = "encryptedData")]
301 pub encrypted_data: HashMap<String, String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub template: Option<SealedSecretTemplate>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct SealedSecretTemplate {
308 pub metadata: SecretMetadata,
309}
310
311impl SealedSecretManifest {
312 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
314 serde_yaml::from_str(yaml)
315 }
316
317 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
319 serde_yaml::to_string(self)
320 }
321
322 pub fn validate_against_request(
324 &self,
325 request: &SecretRequest,
326 ) -> Result<(), crate::ValidationError> {
327 if self.metadata.name != request.name {
329 return Err(crate::ValidationError::Other(format!(
330 "Output name mismatch: expected '{}', got '{}'",
331 request.name, self.metadata.name
332 )));
333 }
334
335 if self.metadata.namespace != request.namespace {
336 return Err(crate::ValidationError::Other(format!(
337 "Output namespace mismatch: expected '{}', got '{}'",
338 request.namespace, self.metadata.namespace
339 )));
340 }
341
342 for kv in &request.data {
344 if !self.spec.encrypted_data.contains_key(&kv.key) {
345 return Err(crate::ValidationError::Other(format!(
346 "Missing key in output: '{}'",
347 kv.key
348 )));
349 }
350 }
351
352 Ok(())
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use std::collections::HashMap;
360
361 #[test]
362 fn test_secret_key_value_new() {
363 let kv = SecretKeyValue::new("my-key".to_string(), "my-value".to_string());
364 assert!(kv.is_ok());
365 let kv = kv.unwrap();
366 assert_eq!(kv.key, "my-key");
367 assert_eq!(*kv.value, "my-value");
368 }
369
370 #[test]
371 fn test_secret_key_value_empty_value() {
372 let kv = SecretKeyValue::new("my-key".to_string(), "".to_string());
373 assert!(kv.is_err());
374 assert!(kv
375 .unwrap_err()
376 .to_string()
377 .contains("value cannot be empty"));
378 }
379
380 #[test]
381 fn test_secret_key_value_invalid_key() {
382 let kv = SecretKeyValue::new(".invalid-key".to_string(), "value".to_string());
384 assert!(kv.is_err());
385 }
386
387 #[test]
388 fn test_validate_secret_name_valid() {
389 assert!(validate_secret_name("my-secret").is_ok());
390 assert!(validate_secret_name("my.secret").is_ok());
391 assert!(validate_secret_name("secret123").is_ok());
392 assert!(validate_secret_name("a").is_ok());
393 assert!(validate_secret_name("a-b-c").is_ok());
394 }
395
396 #[test]
397 fn test_validate_secret_name_empty() {
398 let result = validate_secret_name("");
399 assert!(result.is_err());
400 assert!(result.unwrap_err().to_string().contains("1-253 characters"));
401 }
402
403 #[test]
404 fn test_validate_secret_name_too_long() {
405 let long_name = "a".repeat(254);
406 let result = validate_secret_name(&long_name);
407 assert!(result.is_err());
408 assert!(result.unwrap_err().to_string().contains("1-253 characters"));
409 }
410
411 #[test]
412 fn test_validate_secret_name_starts_with_hyphen() {
413 let result = validate_secret_name("-secret");
414 assert!(result.is_err());
415 assert!(result
416 .unwrap_err()
417 .to_string()
418 .contains("must start with alphanumeric"));
419 }
420
421 #[test]
422 fn test_validate_secret_name_ends_with_hyphen() {
423 let result = validate_secret_name("secret-");
424 assert!(result.is_err());
425 assert!(result
426 .unwrap_err()
427 .to_string()
428 .contains("must end with alphanumeric"));
429 }
430
431 #[test]
432 fn test_validate_secret_name_uppercase() {
433 let result = validate_secret_name("MySecret");
434 assert!(result.is_err());
435 assert!(result
436 .unwrap_err()
437 .to_string()
438 .contains("lowercase alphanumeric"));
439 }
440
441 #[test]
442 fn test_validate_namespace_valid() {
443 assert!(validate_namespace("default").is_ok());
444 assert!(validate_namespace("my-namespace").is_ok());
445 assert!(validate_namespace("ns123").is_ok());
446 }
447
448 #[test]
449 fn test_validate_namespace_empty() {
450 let result = validate_namespace("");
451 assert!(result.is_err());
452 assert!(result.unwrap_err().to_string().contains("1-63 characters"));
453 }
454
455 #[test]
456 fn test_validate_namespace_too_long() {
457 let long_ns = "a".repeat(64);
458 let result = validate_namespace(&long_ns);
459 assert!(result.is_err());
460 assert!(result.unwrap_err().to_string().contains("1-63 characters"));
461 }
462
463 #[test]
464 fn test_validate_key_name_valid() {
465 assert!(validate_key_name("my-key").is_ok());
466 assert!(validate_key_name("MY_KEY").is_ok());
467 assert!(validate_key_name("KEY_123").is_ok());
468 assert!(validate_key_name("_private").is_ok());
469 }
470
471 #[test]
472 fn test_validate_key_name_starts_with_digit() {
473 let result = validate_key_name("1key");
474 assert!(result.is_err());
475 assert!(result
476 .unwrap_err()
477 .to_string()
478 .contains("must start with letter or underscore"));
479 }
480
481 #[test]
482 fn test_validate_key_name_starts_with_dot() {
483 let result = validate_key_name(".key");
484 assert!(result.is_err());
485 }
486
487 #[test]
488 fn test_validate_key_name_contains_slash() {
489 let result = validate_key_name("key/name");
490 assert!(result.is_err());
491 }
492
493 #[test]
494 fn test_validate_key_name_contains_dot() {
495 let result = validate_key_name("key.name");
497 assert!(result.is_err());
498 }
499
500 #[test]
501 fn test_secret_request_new() {
502 let data = vec![SecretKeyValue::new("key1".to_string(), "value1".to_string()).unwrap()];
503 let request = SecretRequest::new(
504 "my-secret".to_string(),
505 "default".to_string(),
506 HashMap::new(),
507 HashMap::new(),
508 data,
509 );
510 assert!(request.is_ok());
511 }
512
513 #[test]
514 fn test_secret_request_empty_data() {
515 let request = SecretRequest::new(
516 "my-secret".to_string(),
517 "default".to_string(),
518 HashMap::new(),
519 HashMap::new(),
520 vec![],
521 );
522 assert!(request.is_err());
523 assert!(request
524 .unwrap_err()
525 .to_string()
526 .contains("Secret data cannot be empty"));
527 }
528
529 #[test]
530 fn test_secret_request_invalid_name() {
531 let data = vec![SecretKeyValue::new("key1".to_string(), "value1".to_string()).unwrap()];
532 let request = SecretRequest::new(
533 "".to_string(), "default".to_string(),
535 HashMap::new(),
536 HashMap::new(),
537 data,
538 );
539 assert!(request.is_err());
540 }
541
542 #[test]
543 fn test_secret_manifest_from_request() {
544 let data = vec![SecretKeyValue::new("key1".to_string(), "value1".to_string()).unwrap()];
545 let request = SecretRequest::new(
546 "my-secret".to_string(),
547 "default".to_string(),
548 HashMap::new(),
549 HashMap::new(),
550 data,
551 )
552 .unwrap();
553
554 let manifest = SecretManifest::from_request(&request);
555 assert_eq!(manifest.metadata.name, "my-secret");
556 assert_eq!(manifest.metadata.namespace, "default");
557 assert_eq!(manifest.kind, "Secret");
558 assert!(manifest.data.contains_key("key1"));
559 }
560
561 #[test]
562 fn test_sealed_secret_validate_against_request() {
563 let data = vec![SecretKeyValue::new("key1".to_string(), "value1".to_string()).unwrap()];
564 let request = SecretRequest::new(
565 "my-secret".to_string(),
566 "default".to_string(),
567 HashMap::new(),
568 HashMap::new(),
569 data,
570 )
571 .unwrap();
572
573 let mut encrypted_data = HashMap::new();
574 encrypted_data.insert("key1".to_string(), "encrypted-value".to_string());
575
576 let sealed_secret = SealedSecretManifest {
577 api_version: "bitnami.com/v1alpha1".to_string(),
578 kind: "SealedSecret".to_string(),
579 metadata: SecretMetadata {
580 name: "my-secret".to_string(),
581 namespace: "default".to_string(),
582 labels: Some(HashMap::new()),
583 annotations: Some(HashMap::new()),
584 },
585 spec: SealedSecretSpec {
586 encrypted_data,
587 template: None,
588 },
589 };
590
591 assert!(sealed_secret.validate_against_request(&request).is_ok());
592 }
593
594 #[test]
595 fn test_sealed_secret_validate_name_mismatch() {
596 let data = vec![SecretKeyValue::new("key1".to_string(), "value1".to_string()).unwrap()];
597 let request = SecretRequest::new(
598 "my-secret".to_string(),
599 "default".to_string(),
600 HashMap::new(),
601 HashMap::new(),
602 data,
603 )
604 .unwrap();
605
606 let sealed_secret = SealedSecretManifest {
607 api_version: "bitnami.com/v1alpha1".to_string(),
608 kind: "SealedSecret".to_string(),
609 metadata: SecretMetadata {
610 name: "wrong-name".to_string(),
611 namespace: "default".to_string(),
612 labels: Some(HashMap::new()),
613 annotations: Some(HashMap::new()),
614 },
615 spec: SealedSecretSpec {
616 encrypted_data: HashMap::new(),
617 template: None,
618 },
619 };
620
621 let result = sealed_secret.validate_against_request(&request);
622 assert!(result.is_err());
623 assert!(result.unwrap_err().to_string().contains("name mismatch"));
624 }
625
626 #[test]
627 fn test_sealed_secret_validate_missing_key() {
628 let data = vec![
629 SecretKeyValue::new("key1".to_string(), "value1".to_string()).unwrap(),
630 SecretKeyValue::new("key2".to_string(), "value2".to_string()).unwrap(),
631 ];
632 let request = SecretRequest::new(
633 "my-secret".to_string(),
634 "default".to_string(),
635 HashMap::new(),
636 HashMap::new(),
637 data,
638 )
639 .unwrap();
640
641 let mut encrypted_data = HashMap::new();
642 encrypted_data.insert("key1".to_string(), "encrypted-value".to_string());
643 let sealed_secret = SealedSecretManifest {
646 api_version: "bitnami.com/v1alpha1".to_string(),
647 kind: "SealedSecret".to_string(),
648 metadata: SecretMetadata {
649 name: "my-secret".to_string(),
650 namespace: "default".to_string(),
651 labels: Some(HashMap::new()),
652 annotations: Some(HashMap::new()),
653 },
654 spec: SealedSecretSpec {
655 encrypted_data,
656 template: None,
657 },
658 };
659
660 let result = sealed_secret.validate_against_request(&request);
661 assert!(result.is_err());
662 assert!(result.unwrap_err().to_string().contains("Missing key"));
663 }
664}