mockforge_core/security/
change_management.rs

1//! Change Management System
2//!
3//! This module provides a formal change management process for system changes,
4//! ensuring all changes are properly planned, approved, tested, and documented.
5
6use crate::Error;
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// Change type classification
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[serde(rename_all = "snake_case")]
16pub enum ChangeType {
17    /// Security enhancement
18    Security,
19    /// Feature addition
20    Feature,
21    /// Bug fix
22    Bugfix,
23    /// Infrastructure change
24    Infrastructure,
25    /// Configuration change
26    Configuration,
27}
28
29/// Change priority
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
31#[serde(rename_all = "lowercase")]
32pub enum ChangePriority {
33    /// Critical priority - immediate action required
34    Critical,
35    /// High priority - urgent action required
36    High,
37    /// Medium priority - action required
38    Medium,
39    /// Low priority - planned action
40    Low,
41}
42
43/// Change urgency
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum ChangeUrgency {
47    /// Emergency - critical security fixes, system outages
48    Emergency,
49    /// High urgency
50    High,
51    /// Medium urgency
52    Medium,
53    /// Low urgency
54    Low,
55}
56
57/// Change status
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum ChangeStatus {
61    /// Change request pending approval
62    PendingApproval,
63    /// Change approved, ready for implementation
64    Approved,
65    /// Change rejected
66    Rejected,
67    /// Change being implemented
68    Implementing,
69    /// Change completed
70    Completed,
71    /// Change cancelled
72    Cancelled,
73    /// Change rolled back
74    RolledBack,
75}
76
77/// Change request
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ChangeRequest {
80    /// Change request ID (e.g., "CHG-2025-001")
81    pub change_id: String,
82    /// Change title
83    pub title: String,
84    /// Change description
85    pub description: String,
86    /// Requester user ID
87    pub requester_id: Uuid,
88    /// Request date
89    pub request_date: DateTime<Utc>,
90    /// Change type
91    pub change_type: ChangeType,
92    /// Change priority
93    pub priority: ChangePriority,
94    /// Change urgency
95    pub urgency: ChangeUrgency,
96    /// Affected systems
97    pub affected_systems: Vec<String>,
98    /// Impact scope
99    pub impact_scope: Option<String>,
100    /// Risk level
101    pub risk_level: Option<String>,
102    /// Rollback plan
103    pub rollback_plan: Option<String>,
104    /// Testing required
105    pub testing_required: bool,
106    /// Test plan
107    pub test_plan: Option<String>,
108    /// Test environment
109    pub test_environment: Option<String>,
110    /// Change status
111    pub status: ChangeStatus,
112    /// Approvers required
113    pub approvers: Vec<String>,
114    /// Approval status (map of approver -> approval status)
115    pub approval_status: HashMap<String, ApprovalStatus>,
116    /// Implementation plan
117    pub implementation_plan: Option<String>,
118    /// Scheduled implementation time
119    pub scheduled_time: Option<DateTime<Utc>>,
120    /// Implementation started time
121    pub implementation_started: Option<DateTime<Utc>>,
122    /// Implementation completed time
123    pub implementation_completed: Option<DateTime<Utc>>,
124    /// Test results
125    pub test_results: Option<String>,
126    /// Post-implementation review
127    pub post_implementation_review: Option<String>,
128    /// Change history
129    pub history: Vec<ChangeHistoryEntry>,
130}
131
132/// Approval status for an approver
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "lowercase")]
135pub enum ApprovalStatus {
136    /// Pending approval
137    Pending,
138    /// Approved
139    Approved,
140    /// Rejected
141    Rejected,
142}
143
144/// Change history entry
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ChangeHistoryEntry {
147    /// Timestamp
148    pub timestamp: DateTime<Utc>,
149    /// Action performed
150    pub action: String,
151    /// User who performed the action
152    pub user_id: Uuid,
153    /// Details
154    pub details: String,
155}
156
157impl ChangeRequest {
158    /// Create a new change request
159    pub fn new(
160        change_id: String,
161        title: String,
162        description: String,
163        requester_id: Uuid,
164        change_type: ChangeType,
165        priority: ChangePriority,
166        urgency: ChangeUrgency,
167        affected_systems: Vec<String>,
168        testing_required: bool,
169        approvers: Vec<String>,
170    ) -> Self {
171        let now = Utc::now();
172        let mut approval_status = HashMap::new();
173        for approver in &approvers {
174            approval_status.insert(approver.clone(), ApprovalStatus::Pending);
175        }
176
177        Self {
178            change_id,
179            title,
180            description,
181            requester_id,
182            request_date: now,
183            change_type,
184            priority,
185            urgency,
186            affected_systems,
187            impact_scope: None,
188            risk_level: None,
189            rollback_plan: None,
190            testing_required,
191            test_plan: None,
192            test_environment: None,
193            status: ChangeStatus::PendingApproval,
194            approvers,
195            approval_status,
196            implementation_plan: None,
197            scheduled_time: None,
198            implementation_started: None,
199            implementation_completed: None,
200            test_results: None,
201            post_implementation_review: None,
202            history: vec![ChangeHistoryEntry {
203                timestamp: now,
204                action: "created".to_string(),
205                user_id: requester_id,
206                details: "Change request created".to_string(),
207            }],
208        }
209    }
210
211    /// Check if all approvals are complete
212    pub fn is_fully_approved(&self) -> bool {
213        self.approval_status.values().all(|status| *status == ApprovalStatus::Approved)
214    }
215
216    /// Check if any approval was rejected
217    pub fn is_rejected(&self) -> bool {
218        self.approval_status.values().any(|status| *status == ApprovalStatus::Rejected)
219    }
220
221    /// Add history entry
222    pub fn add_history(&mut self, action: String, user_id: Uuid, details: String) {
223        self.history.push(ChangeHistoryEntry {
224            timestamp: Utc::now(),
225            action,
226            user_id,
227            details,
228        });
229    }
230}
231
232/// Change management configuration
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235pub struct ChangeManagementConfig {
236    /// Whether change management is enabled
237    pub enabled: bool,
238    /// Approval workflow configuration
239    pub approval_workflow: ApprovalWorkflowConfig,
240    /// Testing requirements
241    pub testing: TestingConfig,
242    /// Notification configuration
243    pub notifications: NotificationConfig,
244}
245
246/// Approval workflow configuration
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249pub struct ApprovalWorkflowConfig {
250    /// Emergency change approvers
251    pub emergency: ApprovalLevelConfig,
252    /// High priority approvers
253    pub high: ApprovalLevelConfig,
254    /// Medium priority approvers
255    pub medium: ApprovalLevelConfig,
256    /// Low priority approvers
257    pub low: ApprovalLevelConfig,
258}
259
260/// Approval level configuration
261#[derive(Debug, Clone, Serialize, Deserialize)]
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263pub struct ApprovalLevelConfig {
264    /// Required approvers
265    pub approvers: Vec<String>,
266    /// Approval timeout (in hours)
267    pub approval_timeout_hours: u64,
268}
269
270/// Testing configuration
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273pub struct TestingConfig {
274    /// Change types that require testing
275    pub required_for: Vec<ChangeType>,
276    /// Test environments
277    pub test_environments: Vec<String>,
278    /// Required test coverage percentage
279    pub test_coverage_required: u8,
280}
281
282/// Notification configuration
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285pub struct NotificationConfig {
286    /// Whether notifications are enabled
287    pub enabled: bool,
288    /// Notification channels
289    pub channels: Vec<String>,
290    /// Recipients
291    pub recipients: Vec<String>,
292}
293
294impl Default for ChangeManagementConfig {
295    fn default() -> Self {
296        Self {
297            enabled: true,
298            approval_workflow: ApprovalWorkflowConfig {
299                emergency: ApprovalLevelConfig {
300                    approvers: vec![
301                        "security-team-lead".to_string(),
302                        "engineering-manager".to_string(),
303                    ],
304                    approval_timeout_hours: 1,
305                },
306                high: ApprovalLevelConfig {
307                    approvers: vec![
308                        "security-team".to_string(),
309                        "engineering-manager".to_string(),
310                        "change-manager".to_string(),
311                    ],
312                    approval_timeout_hours: 24,
313                },
314                medium: ApprovalLevelConfig {
315                    approvers: vec![
316                        "engineering-manager".to_string(),
317                        "change-manager".to_string(),
318                    ],
319                    approval_timeout_hours: 72,
320                },
321                low: ApprovalLevelConfig {
322                    approvers: vec!["change-manager".to_string()],
323                    approval_timeout_hours: 168, // 7 days
324                },
325            },
326            testing: TestingConfig {
327                required_for: vec![ChangeType::Security, ChangeType::Infrastructure],
328                test_environments: vec!["staging".to_string(), "production-like".to_string()],
329                test_coverage_required: 80,
330            },
331            notifications: NotificationConfig {
332                enabled: true,
333                channels: vec!["email".to_string(), "slack".to_string()],
334                recipients: vec![
335                    "change-manager".to_string(),
336                    "security-team".to_string(),
337                    "engineering-team".to_string(),
338                ],
339            },
340        }
341    }
342}
343
344/// Change management engine
345pub struct ChangeManagementEngine {
346    config: ChangeManagementConfig,
347    /// Active change requests
348    changes: std::sync::Arc<tokio::sync::RwLock<HashMap<String, ChangeRequest>>>,
349    /// Change ID counter
350    change_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
351}
352
353impl ChangeManagementEngine {
354    /// Create a new change management engine
355    pub fn new(config: ChangeManagementConfig) -> Self {
356        Self {
357            config,
358            changes: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
359            change_id_counter: std::sync::Arc::new(tokio::sync::RwLock::new(0)),
360        }
361    }
362
363    /// Generate next change ID
364    async fn generate_change_id(&self) -> String {
365        let now = Utc::now();
366        let year = now.format("%Y").to_string();
367        let mut counter = self.change_id_counter.write().await;
368        *counter += 1;
369        format!("CHG-{}-{:03}", year, *counter)
370    }
371
372    /// Get approvers for a priority level
373    fn get_approvers_for_priority(&self, priority: ChangePriority) -> Vec<String> {
374        match priority {
375            ChangePriority::Critical => self.config.approval_workflow.emergency.approvers.clone(),
376            ChangePriority::High => self.config.approval_workflow.high.approvers.clone(),
377            ChangePriority::Medium => self.config.approval_workflow.medium.approvers.clone(),
378            ChangePriority::Low => self.config.approval_workflow.low.approvers.clone(),
379        }
380    }
381
382    /// Create a new change request
383    pub async fn create_change_request(
384        &self,
385        title: String,
386        description: String,
387        requester_id: Uuid,
388        change_type: ChangeType,
389        priority: ChangePriority,
390        urgency: ChangeUrgency,
391        affected_systems: Vec<String>,
392        testing_required: bool,
393        test_plan: Option<String>,
394        test_environment: Option<String>,
395        rollback_plan: Option<String>,
396        impact_scope: Option<String>,
397        risk_level: Option<String>,
398    ) -> Result<ChangeRequest, Error> {
399        let change_id = self.generate_change_id().await;
400        let approvers = self.get_approvers_for_priority(priority);
401
402        let mut change = ChangeRequest::new(
403            change_id,
404            title,
405            description,
406            requester_id,
407            change_type,
408            priority,
409            urgency,
410            affected_systems,
411            testing_required,
412            approvers,
413        );
414
415        change.test_plan = test_plan;
416        change.test_environment = test_environment;
417        change.rollback_plan = rollback_plan;
418        change.impact_scope = impact_scope;
419        change.risk_level = risk_level;
420
421        let change_id = change.change_id.clone();
422        let mut changes = self.changes.write().await;
423        changes.insert(change_id, change.clone());
424
425        Ok(change)
426    }
427
428    /// Approve a change request
429    pub async fn approve_change(
430        &self,
431        change_id: &str,
432        approver: &str,
433        approver_id: Uuid,
434        comments: Option<String>,
435        conditions: Option<Vec<String>>,
436    ) -> Result<(), Error> {
437        let mut changes = self.changes.write().await;
438        let change = changes
439            .get_mut(change_id)
440            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
441
442        if change.status != ChangeStatus::PendingApproval {
443            return Err(Error::Generic("Change request is not pending approval".to_string()));
444        }
445
446        if !change.approvers.contains(&approver.to_string()) {
447            return Err(Error::Generic("User is not an approver for this change".to_string()));
448        }
449
450        change.approval_status.insert(approver.to_string(), ApprovalStatus::Approved);
451
452        let details = format!(
453            "Change approved by {}{}{}",
454            approver,
455            comments.map(|c| format!(" - {}", c)).unwrap_or_default(),
456            conditions
457                .map(|conds| format!(" - Conditions: {}", conds.join(", ")))
458                .unwrap_or_default()
459        );
460        change.add_history("approved".to_string(), approver_id, details);
461
462        // Check if all approvals are complete
463        if change.is_fully_approved() {
464            change.status = ChangeStatus::Approved;
465            change.add_history(
466                "all_approvals_complete".to_string(),
467                approver_id,
468                "All approvals received, change ready for implementation".to_string(),
469            );
470        }
471
472        Ok(())
473    }
474
475    /// Reject a change request
476    pub async fn reject_change(
477        &self,
478        change_id: &str,
479        approver: &str,
480        approver_id: Uuid,
481        reason: String,
482    ) -> Result<(), Error> {
483        let mut changes = self.changes.write().await;
484        let change = changes
485            .get_mut(change_id)
486            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
487
488        if change.status != ChangeStatus::PendingApproval {
489            return Err(Error::Generic("Change request is not pending approval".to_string()));
490        }
491
492        change.approval_status.insert(approver.to_string(), ApprovalStatus::Rejected);
493        change.status = ChangeStatus::Rejected;
494        change.add_history(
495            "rejected".to_string(),
496            approver_id,
497            format!("Change rejected: {}", reason),
498        );
499
500        Ok(())
501    }
502
503    /// Start change implementation
504    pub async fn start_implementation(
505        &self,
506        change_id: &str,
507        implementer_id: Uuid,
508        implementation_plan: String,
509        scheduled_time: Option<DateTime<Utc>>,
510    ) -> Result<(), Error> {
511        let mut changes = self.changes.write().await;
512        let change = changes
513            .get_mut(change_id)
514            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
515
516        if change.status != ChangeStatus::Approved {
517            return Err(Error::Generic(
518                "Change request must be approved before implementation".to_string(),
519            ));
520        }
521
522        change.status = ChangeStatus::Implementing;
523        change.implementation_plan = Some(implementation_plan);
524        change.scheduled_time = scheduled_time;
525        change.implementation_started = Some(Utc::now());
526
527        change.add_history(
528            "implementation_started".to_string(),
529            implementer_id,
530            "Change implementation started".to_string(),
531        );
532
533        Ok(())
534    }
535
536    /// Complete change implementation
537    pub async fn complete_change(
538        &self,
539        change_id: &str,
540        implementer_id: Uuid,
541        test_results: Option<String>,
542        post_implementation_review: Option<String>,
543    ) -> Result<(), Error> {
544        let mut changes = self.changes.write().await;
545        let change = changes
546            .get_mut(change_id)
547            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
548
549        if change.status != ChangeStatus::Implementing {
550            return Err(Error::Generic(
551                "Change request must be in implementing status".to_string(),
552            ));
553        }
554
555        change.status = ChangeStatus::Completed;
556        change.implementation_completed = Some(Utc::now());
557        change.test_results = test_results;
558        change.post_implementation_review = post_implementation_review;
559
560        change.add_history(
561            "completed".to_string(),
562            implementer_id,
563            "Change implementation completed".to_string(),
564        );
565
566        Ok(())
567    }
568
569    /// Get change request by ID
570    pub async fn get_change(&self, change_id: &str) -> Result<Option<ChangeRequest>, Error> {
571        let changes = self.changes.read().await;
572        Ok(changes.get(change_id).cloned())
573    }
574
575    /// Get all change requests
576    pub async fn get_all_changes(&self) -> Result<Vec<ChangeRequest>, Error> {
577        let changes = self.changes.read().await;
578        Ok(changes.values().cloned().collect())
579    }
580
581    /// Get changes by status
582    pub async fn get_changes_by_status(
583        &self,
584        status: ChangeStatus,
585    ) -> Result<Vec<ChangeRequest>, Error> {
586        let changes = self.changes.read().await;
587        Ok(changes.values().filter(|c| c.status == status).cloned().collect())
588    }
589
590    /// Get changes by requester
591    pub async fn get_changes_by_requester(
592        &self,
593        requester_id: Uuid,
594    ) -> Result<Vec<ChangeRequest>, Error> {
595        let changes = self.changes.read().await;
596        Ok(changes.values().filter(|c| c.requester_id == requester_id).cloned().collect())
597    }
598
599    /// Cancel a change request
600    pub async fn cancel_change(
601        &self,
602        change_id: &str,
603        user_id: Uuid,
604        reason: String,
605    ) -> Result<(), Error> {
606        let mut changes = self.changes.write().await;
607        let change = changes
608            .get_mut(change_id)
609            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
610
611        change.status = ChangeStatus::Cancelled;
612        change.add_history(
613            "cancelled".to_string(),
614            user_id,
615            format!("Change cancelled: {}", reason),
616        );
617
618        Ok(())
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625
626    #[tokio::test]
627    async fn test_change_request_creation() {
628        let config = ChangeManagementConfig::default();
629        let engine = ChangeManagementEngine::new(config);
630
631        let change = engine
632            .create_change_request(
633                "Test Change".to_string(),
634                "Test description".to_string(),
635                Uuid::new_v4(),
636                ChangeType::Security,
637                ChangePriority::High,
638                ChangeUrgency::High,
639                vec!["system1".to_string()],
640                true,
641                Some("Test plan".to_string()),
642                Some("staging".to_string()),
643                None,
644                None,
645                None,
646            )
647            .await
648            .unwrap();
649
650        assert_eq!(change.status, ChangeStatus::PendingApproval);
651        assert!(!change.approvers.is_empty());
652    }
653}