1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum RotationStatus {
12 Healthy,
14 ExpiringSoon,
16 NeedsRotation,
18 Overdue,
20}
21
22impl std::fmt::Display for RotationStatus {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 Self::Healthy => write!(f, "healthy"),
26 Self::ExpiringSoon => write!(f, "expiring_soon"),
27 Self::NeedsRotation => write!(f, "needs_rotation"),
28 Self::Overdue => write!(f, "overdue"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct RotationStatusResponse {
36 pub current_version: u16,
38 pub ttl_days: u32,
40 pub last_rotation: Option<DateTime<Utc>>,
42 pub next_rotation: Option<DateTime<Utc>>,
44 pub status: RotationStatus,
46 pub auto_refresh_enabled: bool,
48 pub versions_total: usize,
50 pub versions_active: usize,
52 pub versions_expired: usize,
54 pub last_rotation_duration_ms: u64,
56 pub auto_refresh_checks: u64,
58}
59
60impl RotationStatusResponse {
61 pub fn new(current_version: u16, ttl_days: u32) -> Self {
63 Self {
64 current_version,
65 ttl_days,
66 last_rotation: None,
67 next_rotation: None,
68 status: RotationStatus::Healthy,
69 auto_refresh_enabled: true,
70 versions_total: 1,
71 versions_active: 1,
72 versions_expired: 0,
73 last_rotation_duration_ms: 0,
74 auto_refresh_checks: 0,
75 }
76 }
77
78 pub fn with_last_rotation(mut self, timestamp: DateTime<Utc>) -> Self {
80 self.last_rotation = Some(timestamp);
81 self
82 }
83
84 pub fn with_next_rotation(mut self, timestamp: DateTime<Utc>) -> Self {
86 self.next_rotation = Some(timestamp);
87 self
88 }
89
90 pub fn with_status(mut self, status: RotationStatus) -> Self {
92 self.status = status;
93 self
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ManualRotationRequest {
100 pub key_id: Option<String>,
102 pub reason: Option<String>,
104 pub dry_run: Option<bool>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ManualRotationResponse {
111 pub new_version: u16,
113 pub old_version: u16,
115 pub status: String,
117 pub duration_ms: u64,
119 pub error: Option<String>,
121}
122
123impl ManualRotationResponse {
124 pub fn success(old_version: u16, new_version: u16, duration_ms: u64) -> Self {
126 Self {
127 new_version,
128 old_version,
129 status: "success".to_string(),
130 duration_ms,
131 error: None,
132 }
133 }
134
135 pub fn failure(old_version: u16, error: impl Into<String>) -> Self {
137 Self {
138 new_version: old_version,
139 old_version,
140 status: "failed".to_string(),
141 duration_ms: 0,
142 error: Some(error.into()),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct RotationHistoryRecord {
150 pub timestamp: DateTime<Utc>,
152 pub old_version: u16,
154 pub new_version: u16,
156 pub reason: Option<String>,
158 pub duration_ms: u64,
160 pub triggered_by: String,
162 pub user_id: Option<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct RotationHistoryResponse {
169 pub total_count: usize,
171 pub offset: usize,
173 pub limit: usize,
175 pub records: Vec<RotationHistoryRecord>,
177}
178
179impl RotationHistoryResponse {
180 pub fn new(offset: usize, limit: usize) -> Self {
182 Self {
183 total_count: 0,
184 offset,
185 limit,
186 records: Vec::new(),
187 }
188 }
189
190 pub fn with_record(mut self, record: RotationHistoryRecord) -> Self {
192 self.records.push(record);
193 self
194 }
195
196 pub fn with_total_count(mut self, count: usize) -> Self {
198 self.total_count = count;
199 self
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct RotationConfigResponse {
206 pub auto_refresh_enabled: bool,
208 pub refresh_check_interval_hours: u32,
210 pub refresh_threshold_percent: u32,
212 pub ttl_days: u32,
214 pub quiet_hours_start: Option<u32>,
216 pub quiet_hours_end: Option<u32>,
218 pub manual_rotation_cooldown_minutes: u32,
220}
221
222impl RotationConfigResponse {
223 pub fn default() -> Self {
225 Self {
226 auto_refresh_enabled: true,
227 refresh_check_interval_hours: 24,
228 refresh_threshold_percent: 80,
229 ttl_days: 365,
230 quiet_hours_start: None,
231 quiet_hours_end: None,
232 manual_rotation_cooldown_minutes: 60,
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct RotationConfigUpdateRequest {
240 pub auto_refresh_enabled: Option<bool>,
242 pub refresh_check_interval_hours: Option<u32>,
244 pub refresh_threshold_percent: Option<u32>,
246 pub ttl_days: Option<u32>,
248 pub quiet_hours_start: Option<u32>,
250 pub quiet_hours_end: Option<u32>,
252}
253
254impl RotationConfigUpdateRequest {
255 pub fn validate(&self) -> Result<(), String> {
257 if let Some(threshold) = self.refresh_threshold_percent {
258 if threshold < 1 || threshold > 99 {
259 return Err("Threshold must be 1-99".to_string());
260 }
261 }
262
263 if let Some(ttl) = self.ttl_days {
264 if ttl < 1 || ttl > 365 {
265 return Err("TTL must be 1-365 days".to_string());
266 }
267 }
268
269 if let Some(interval) = self.refresh_check_interval_hours {
270 if interval < 1 || interval > 720 {
271 return Err("Interval must be 1-720 hours".to_string());
272 }
273 }
274
275 Ok(())
276 }
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
281#[serde(rename_all = "snake_case")]
282pub enum ScheduleType {
283 Manual,
285 Cron,
287 Interval,
289}
290
291impl std::fmt::Display for ScheduleType {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 match self {
294 Self::Manual => write!(f, "manual"),
295 Self::Cron => write!(f, "cron"),
296 Self::Interval => write!(f, "interval"),
297 }
298 }
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct RotationScheduleResponse {
304 pub schedule_type: ScheduleType,
306 pub schedule_value: String,
308 pub next_scheduled_time: Option<DateTime<Utc>>,
310 pub enabled: bool,
312}
313
314impl RotationScheduleResponse {
315 pub fn manual() -> Self {
317 Self {
318 schedule_type: ScheduleType::Manual,
319 schedule_value: "manual".to_string(),
320 next_scheduled_time: None,
321 enabled: false,
322 }
323 }
324
325 pub fn cron(expression: impl Into<String>, next_time: DateTime<Utc>) -> Self {
327 Self {
328 schedule_type: ScheduleType::Cron,
329 schedule_value: expression.into(),
330 next_scheduled_time: Some(next_time),
331 enabled: true,
332 }
333 }
334
335 pub fn interval(days: u32, next_time: DateTime<Utc>) -> Self {
337 Self {
338 schedule_type: ScheduleType::Interval,
339 schedule_value: format!("{} days", days),
340 next_scheduled_time: Some(next_time),
341 enabled: true,
342 }
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct RotationScheduleUpdateRequest {
349 pub schedule_type: ScheduleType,
351 pub schedule_value: String,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct TestScheduleResponse {
358 pub valid: bool,
360 pub error: Option<String>,
362 pub next_times: Vec<DateTime<Utc>>,
364}
365
366impl TestScheduleResponse {
367 pub fn valid(next_times: Vec<DateTime<Utc>>) -> Self {
369 Self {
370 valid: true,
371 error: None,
372 next_times,
373 }
374 }
375
376 pub fn invalid(error: impl Into<String>) -> Self {
378 Self {
379 valid: false,
380 error: Some(error.into()),
381 next_times: Vec::new(),
382 }
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct ApiErrorResponse {
389 pub error: String,
391 pub message: String,
393 pub code: Option<String>,
395}
396
397impl ApiErrorResponse {
398 pub fn new(error: impl Into<String>, message: impl Into<String>) -> Self {
400 Self {
401 error: error.into(),
402 message: message.into(),
403 code: None,
404 }
405 }
406
407 pub fn with_code(mut self, code: impl Into<String>) -> Self {
409 self.code = Some(code.into());
410 self
411 }
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
418#[serde(rename_all = "snake_case")]
419pub enum ConfigPreset {
420 Hipaa,
422 PciDss,
424 Gdpr,
426 Soc2,
428 Custom,
430}
431
432impl std::fmt::Display for ConfigPreset {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 match self {
435 Self::Hipaa => write!(f, "hipaa"),
436 Self::PciDss => write!(f, "pci_dss"),
437 Self::Gdpr => write!(f, "gdpr"),
438 Self::Soc2 => write!(f, "soc2"),
439 Self::Custom => write!(f, "custom"),
440 }
441 }
442}
443
444impl ConfigPreset {
445 pub fn get_config(&self) -> RotationConfigResponse {
447 match self {
448 Self::Hipaa => RotationConfigResponse {
449 auto_refresh_enabled: true,
450 refresh_check_interval_hours: 24,
451 refresh_threshold_percent: 80,
452 ttl_days: 365,
453 quiet_hours_start: Some(2),
454 quiet_hours_end: Some(4),
455 manual_rotation_cooldown_minutes: 60,
456 },
457 Self::PciDss => RotationConfigResponse {
458 auto_refresh_enabled: true,
459 refresh_check_interval_hours: 24,
460 refresh_threshold_percent: 80,
461 ttl_days: 365,
462 quiet_hours_start: Some(2),
463 quiet_hours_end: Some(4),
464 manual_rotation_cooldown_minutes: 60,
465 },
466 Self::Gdpr => RotationConfigResponse {
467 auto_refresh_enabled: true,
468 refresh_check_interval_hours: 24,
469 refresh_threshold_percent: 75,
470 ttl_days: 90,
471 quiet_hours_start: None,
472 quiet_hours_end: None,
473 manual_rotation_cooldown_minutes: 30,
474 },
475 Self::Soc2 => RotationConfigResponse {
476 auto_refresh_enabled: true,
477 refresh_check_interval_hours: 24,
478 refresh_threshold_percent: 80,
479 ttl_days: 365,
480 quiet_hours_start: Some(2),
481 quiet_hours_end: Some(4),
482 manual_rotation_cooldown_minutes: 60,
483 },
484 Self::Custom => RotationConfigResponse::default(),
485 }
486 }
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct CompliancePresetsResponse {
492 pub presets: Vec<PresetInfo>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct PresetInfo {
499 pub name: ConfigPreset,
501 pub description: String,
503 pub ttl_days: u32,
505 pub check_interval_hours: u32,
507 pub threshold_percent: u32,
509}
510
511impl CompliancePresetsResponse {
512 pub fn default() -> Self {
514 Self {
515 presets: vec![
516 PresetInfo {
517 name: ConfigPreset::Hipaa,
518 description: "HIPAA compliance requirements".to_string(),
519 ttl_days: 365,
520 check_interval_hours: 24,
521 threshold_percent: 80,
522 },
523 PresetInfo {
524 name: ConfigPreset::PciDss,
525 description: "PCI-DSS compliance requirements".to_string(),
526 ttl_days: 365,
527 check_interval_hours: 24,
528 threshold_percent: 80,
529 },
530 PresetInfo {
531 name: ConfigPreset::Gdpr,
532 description: "GDPR data minimization requirements".to_string(),
533 ttl_days: 90,
534 check_interval_hours: 24,
535 threshold_percent: 75,
536 },
537 PresetInfo {
538 name: ConfigPreset::Soc2,
539 description: "SOC 2 compliance requirements".to_string(),
540 ttl_days: 365,
541 check_interval_hours: 24,
542 threshold_percent: 80,
543 },
544 ],
545 }
546 }
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct RotationHistoryQuery {
552 pub limit: Option<usize>,
554 pub offset: Option<usize>,
556 pub from: Option<String>,
558 pub to: Option<String>,
560 pub reason: Option<String>,
562 pub triggered_by: Option<String>,
564 pub format: Option<String>,
566}
567
568impl RotationHistoryQuery {
569 pub fn effective_limit(&self) -> usize {
571 self.limit.unwrap_or(100).min(1000)
572 }
573
574 pub fn effective_offset(&self) -> usize {
576 self.offset.unwrap_or(0)
577 }
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct RotationStatusDisplay {
583 pub status: RotationStatusResponse,
585 pub urgency_score: u32,
587 pub recommended_action: String,
589}
590
591impl RotationStatusDisplay {
592 pub fn from_status(status: RotationStatusResponse) -> Self {
594 let urgency_score = match status.status {
596 RotationStatus::Healthy => 10,
597 RotationStatus::ExpiringSoon => 50,
598 RotationStatus::NeedsRotation => 85,
599 RotationStatus::Overdue => 100,
600 };
601
602 let recommended_action = match status.status {
603 RotationStatus::Healthy => "Monitor key health".to_string(),
604 RotationStatus::ExpiringSoon => "Prepare for upcoming rotation".to_string(),
605 RotationStatus::NeedsRotation => "Trigger manual rotation immediately".to_string(),
606 RotationStatus::Overdue => "CRITICAL: Rotate immediately to prevent expiry".to_string(),
607 };
608
609 Self {
610 status,
611 urgency_score,
612 recommended_action,
613 }
614 }
615}
616
617impl RotationStatusResponse {
618 pub fn to_display(&self) -> RotationStatusDisplay {
620 RotationStatusDisplay::from_status(self.clone())
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn test_rotation_status_response_creation() {
630 let response = RotationStatusResponse::new(1, 365);
631 assert_eq!(response.current_version, 1);
632 assert_eq!(response.ttl_days, 365);
633 assert_eq!(response.status, RotationStatus::Healthy);
634 }
635
636 #[test]
637 fn test_rotation_status_response_builder() {
638 let now = Utc::now();
639 let response = RotationStatusResponse::new(2, 365)
640 .with_last_rotation(now)
641 .with_status(RotationStatus::NeedsRotation);
642 assert!(response.last_rotation.is_some());
643 assert_eq!(response.status, RotationStatus::NeedsRotation);
644 }
645
646 #[test]
647 fn test_manual_rotation_response_success() {
648 let response = ManualRotationResponse::success(1, 2, 100);
649 assert_eq!(response.status, "success");
650 assert_eq!(response.old_version, 1);
651 assert_eq!(response.new_version, 2);
652 assert!(response.error.is_none());
653 }
654
655 #[test]
656 fn test_manual_rotation_response_failure() {
657 let response = ManualRotationResponse::failure(1, "Vault error");
658 assert_eq!(response.status, "failed");
659 assert!(response.error.is_some());
660 }
661
662 #[test]
663 fn test_rotation_history_response_creation() {
664 let response = RotationHistoryResponse::new(0, 100);
665 assert_eq!(response.offset, 0);
666 assert_eq!(response.limit, 100);
667 assert_eq!(response.total_count, 0);
668 }
669
670 #[test]
671 fn test_rotation_history_record_creation() {
672 let now = Utc::now();
673 let record = RotationHistoryRecord {
674 timestamp: now,
675 old_version: 1,
676 new_version: 2,
677 reason: Some("test".to_string()),
678 duration_ms: 50,
679 triggered_by: "manual".to_string(),
680 user_id: Some("user123".to_string()),
681 };
682 assert_eq!(record.old_version, 1);
683 assert_eq!(record.new_version, 2);
684 }
685
686 #[test]
687 fn test_rotation_config_update_validation() {
688 let update = RotationConfigUpdateRequest {
689 auto_refresh_enabled: Some(true),
690 refresh_check_interval_hours: Some(24),
691 refresh_threshold_percent: Some(80),
692 ttl_days: Some(365),
693 quiet_hours_start: None,
694 quiet_hours_end: None,
695 };
696 assert!(update.validate().is_ok());
697 }
698
699 #[test]
700 fn test_rotation_config_update_invalid_threshold() {
701 let update = RotationConfigUpdateRequest {
702 auto_refresh_enabled: None,
703 refresh_check_interval_hours: None,
704 refresh_threshold_percent: Some(100), ttl_days: None,
706 quiet_hours_start: None,
707 quiet_hours_end: None,
708 };
709 assert!(update.validate().is_err());
710 }
711
712 #[test]
713 fn test_rotation_config_update_invalid_ttl() {
714 let update = RotationConfigUpdateRequest {
715 auto_refresh_enabled: None,
716 refresh_check_interval_hours: None,
717 refresh_threshold_percent: None,
718 ttl_days: Some(400), quiet_hours_start: None,
720 quiet_hours_end: None,
721 };
722 assert!(update.validate().is_err());
723 }
724
725 #[test]
726 fn test_rotation_schedule_response_manual() {
727 let schedule = RotationScheduleResponse::manual();
728 assert_eq!(schedule.schedule_type, ScheduleType::Manual);
729 assert!(!schedule.enabled);
730 }
731
732 #[test]
733 fn test_rotation_schedule_response_cron() {
734 let now = Utc::now();
735 let schedule = RotationScheduleResponse::cron("0 2 1 * *", now);
736 assert_eq!(schedule.schedule_type, ScheduleType::Cron);
737 assert!(schedule.enabled);
738 }
739
740 #[test]
741 fn test_rotation_schedule_response_interval() {
742 let now = Utc::now();
743 let schedule = RotationScheduleResponse::interval(30, now);
744 assert_eq!(schedule.schedule_type, ScheduleType::Interval);
745 assert!(schedule.enabled);
746 }
747
748 #[test]
749 fn test_test_schedule_response_valid() {
750 let times = vec![Utc::now(), Utc::now()];
751 let response = TestScheduleResponse::valid(times.clone());
752 assert!(response.valid);
753 assert_eq!(response.next_times.len(), 2);
754 }
755
756 #[test]
757 fn test_test_schedule_response_invalid() {
758 let response = TestScheduleResponse::invalid("Invalid cron");
759 assert!(!response.valid);
760 assert!(response.error.is_some());
761 }
762
763 #[test]
764 fn test_api_error_response_creation() {
765 let error = ApiErrorResponse::new("rotation_failed", "Vault unreachable");
766 assert_eq!(error.error, "rotation_failed");
767 assert_eq!(error.message, "Vault unreachable");
768 }
769
770 #[test]
771 fn test_api_error_response_with_code() {
772 let error = ApiErrorResponse::new("rotation_failed", "Vault unreachable")
773 .with_code("VAULT_UNAVAILABLE");
774 assert!(error.code.is_some());
775 }
776
777 #[test]
780 fn test_config_preset_hipaa() {
781 let config = ConfigPreset::Hipaa.get_config();
782 assert_eq!(config.ttl_days, 365);
783 assert!(config.auto_refresh_enabled);
784 }
785
786 #[test]
787 fn test_config_preset_gdpr() {
788 let config = ConfigPreset::Gdpr.get_config();
789 assert_eq!(config.ttl_days, 90);
790 assert_eq!(config.refresh_threshold_percent, 75);
791 }
792
793 #[test]
794 fn test_compliance_presets_response() {
795 let presets = CompliancePresetsResponse::default();
796 assert_eq!(presets.presets.len(), 4);
797 }
798
799 #[test]
800 fn test_rotation_history_query_effective_limit() {
801 let query1 = RotationHistoryQuery {
802 limit: Some(50),
803 offset: None,
804 from: None,
805 to: None,
806 reason: None,
807 triggered_by: None,
808 format: None,
809 };
810 assert_eq!(query1.effective_limit(), 50);
811
812 let query2 = RotationHistoryQuery {
813 limit: Some(5000),
814 offset: None,
815 from: None,
816 to: None,
817 reason: None,
818 triggered_by: None,
819 format: None,
820 };
821 assert_eq!(query2.effective_limit(), 1000); }
823
824 #[test]
825 fn test_rotation_status_display_healthy() {
826 let status = RotationStatusResponse::new(1, 365);
827 let display = status.to_display();
828 assert_eq!(display.urgency_score, 10);
829 assert!(display.recommended_action.contains("Monitor"));
830 }
831
832 #[test]
833 fn test_rotation_status_display_urgent() {
834 let status = RotationStatusResponse::new(1, 365).with_status(RotationStatus::Overdue);
835 let display = status.to_display();
836 assert_eq!(display.urgency_score, 100);
837 assert!(display.recommended_action.contains("CRITICAL"));
838 }
839}