Skip to main content

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, 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    #[allow(clippy::too_many_arguments)]
160    pub fn new(
161        change_id: String,
162        title: String,
163        description: String,
164        requester_id: Uuid,
165        change_type: ChangeType,
166        priority: ChangePriority,
167        urgency: ChangeUrgency,
168        affected_systems: Vec<String>,
169        testing_required: bool,
170        approvers: Vec<String>,
171    ) -> Self {
172        let now = Utc::now();
173        let mut approval_status = HashMap::new();
174        for approver in &approvers {
175            approval_status.insert(approver.clone(), ApprovalStatus::Pending);
176        }
177
178        Self {
179            change_id,
180            title,
181            description,
182            requester_id,
183            request_date: now,
184            change_type,
185            priority,
186            urgency,
187            affected_systems,
188            impact_scope: None,
189            risk_level: None,
190            rollback_plan: None,
191            testing_required,
192            test_plan: None,
193            test_environment: None,
194            status: ChangeStatus::PendingApproval,
195            approvers,
196            approval_status,
197            implementation_plan: None,
198            scheduled_time: None,
199            implementation_started: None,
200            implementation_completed: None,
201            test_results: None,
202            post_implementation_review: None,
203            history: vec![ChangeHistoryEntry {
204                timestamp: now,
205                action: "created".to_string(),
206                user_id: requester_id,
207                details: "Change request created".to_string(),
208            }],
209        }
210    }
211
212    /// Check if all approvals are complete
213    pub fn is_fully_approved(&self) -> bool {
214        self.approval_status.values().all(|status| *status == ApprovalStatus::Approved)
215    }
216
217    /// Check if any approval was rejected
218    pub fn is_rejected(&self) -> bool {
219        self.approval_status.values().any(|status| *status == ApprovalStatus::Rejected)
220    }
221
222    /// Add history entry
223    pub fn add_history(&mut self, action: String, user_id: Uuid, details: String) {
224        self.history.push(ChangeHistoryEntry {
225            timestamp: Utc::now(),
226            action,
227            user_id,
228            details,
229        });
230    }
231}
232
233/// Change management configuration
234#[derive(Debug, Clone, Serialize, Deserialize)]
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236pub struct ChangeManagementConfig {
237    /// Whether change management is enabled
238    pub enabled: bool,
239    /// Approval workflow configuration
240    pub approval_workflow: ApprovalWorkflowConfig,
241    /// Testing requirements
242    pub testing: TestingConfig,
243    /// Notification configuration
244    pub notifications: NotificationConfig,
245}
246
247/// Approval workflow configuration
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
250pub struct ApprovalWorkflowConfig {
251    /// Emergency change approvers
252    pub emergency: ApprovalLevelConfig,
253    /// High priority approvers
254    pub high: ApprovalLevelConfig,
255    /// Medium priority approvers
256    pub medium: ApprovalLevelConfig,
257    /// Low priority approvers
258    pub low: ApprovalLevelConfig,
259}
260
261/// Approval level configuration
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
264pub struct ApprovalLevelConfig {
265    /// Required approvers
266    pub approvers: Vec<String>,
267    /// Approval timeout (in hours)
268    pub approval_timeout_hours: u64,
269}
270
271/// Testing configuration
272#[derive(Debug, Clone, Serialize, Deserialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274pub struct TestingConfig {
275    /// Change types that require testing
276    pub required_for: Vec<ChangeType>,
277    /// Test environments
278    pub test_environments: Vec<String>,
279    /// Required test coverage percentage
280    pub test_coverage_required: u8,
281}
282
283/// Notification configuration
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
286pub struct NotificationConfig {
287    /// Whether notifications are enabled
288    pub enabled: bool,
289    /// Notification channels
290    pub channels: Vec<String>,
291    /// Recipients
292    pub recipients: Vec<String>,
293}
294
295impl Default for ChangeManagementConfig {
296    fn default() -> Self {
297        Self {
298            enabled: true,
299            approval_workflow: ApprovalWorkflowConfig {
300                emergency: ApprovalLevelConfig {
301                    approvers: vec![
302                        "security-team-lead".to_string(),
303                        "engineering-manager".to_string(),
304                    ],
305                    approval_timeout_hours: 1,
306                },
307                high: ApprovalLevelConfig {
308                    approvers: vec![
309                        "security-team".to_string(),
310                        "engineering-manager".to_string(),
311                        "change-manager".to_string(),
312                    ],
313                    approval_timeout_hours: 24,
314                },
315                medium: ApprovalLevelConfig {
316                    approvers: vec![
317                        "engineering-manager".to_string(),
318                        "change-manager".to_string(),
319                    ],
320                    approval_timeout_hours: 72,
321                },
322                low: ApprovalLevelConfig {
323                    approvers: vec!["change-manager".to_string()],
324                    approval_timeout_hours: 168, // 7 days
325                },
326            },
327            testing: TestingConfig {
328                required_for: vec![ChangeType::Security, ChangeType::Infrastructure],
329                test_environments: vec!["staging".to_string(), "production-like".to_string()],
330                test_coverage_required: 80,
331            },
332            notifications: NotificationConfig {
333                enabled: true,
334                channels: vec!["email".to_string(), "slack".to_string()],
335                recipients: vec![
336                    "change-manager".to_string(),
337                    "security-team".to_string(),
338                    "engineering-team".to_string(),
339                ],
340            },
341        }
342    }
343}
344
345/// Change management engine
346pub struct ChangeManagementEngine {
347    config: ChangeManagementConfig,
348    /// Active change requests
349    changes: std::sync::Arc<tokio::sync::RwLock<HashMap<String, ChangeRequest>>>,
350    /// Change ID counter
351    change_id_counter: std::sync::Arc<tokio::sync::RwLock<u64>>,
352}
353
354impl ChangeManagementEngine {
355    /// Create a new change management engine
356    pub fn new(config: ChangeManagementConfig) -> Self {
357        Self {
358            config,
359            changes: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
360            change_id_counter: std::sync::Arc::new(tokio::sync::RwLock::new(0)),
361        }
362    }
363
364    /// Generate next change ID
365    async fn generate_change_id(&self) -> String {
366        let now = Utc::now();
367        let year = now.format("%Y").to_string();
368        let mut counter = self.change_id_counter.write().await;
369        *counter += 1;
370        format!("CHG-{}-{:03}", year, *counter)
371    }
372
373    /// Get approvers for a priority level
374    fn get_approvers_for_priority(&self, priority: ChangePriority) -> Vec<String> {
375        match priority {
376            ChangePriority::Critical => self.config.approval_workflow.emergency.approvers.clone(),
377            ChangePriority::High => self.config.approval_workflow.high.approvers.clone(),
378            ChangePriority::Medium => self.config.approval_workflow.medium.approvers.clone(),
379            ChangePriority::Low => self.config.approval_workflow.low.approvers.clone(),
380        }
381    }
382
383    /// Create a new change request
384    #[allow(clippy::too_many_arguments)]
385    pub async fn create_change_request(
386        &self,
387        title: String,
388        description: String,
389        requester_id: Uuid,
390        change_type: ChangeType,
391        priority: ChangePriority,
392        urgency: ChangeUrgency,
393        affected_systems: Vec<String>,
394        testing_required: bool,
395        test_plan: Option<String>,
396        test_environment: Option<String>,
397        rollback_plan: Option<String>,
398        impact_scope: Option<String>,
399        risk_level: Option<String>,
400    ) -> Result<ChangeRequest, Error> {
401        let change_id = self.generate_change_id().await;
402        let approvers = self.get_approvers_for_priority(priority);
403
404        let mut change = ChangeRequest::new(
405            change_id,
406            title,
407            description,
408            requester_id,
409            change_type,
410            priority,
411            urgency,
412            affected_systems,
413            testing_required,
414            approvers,
415        );
416
417        change.test_plan = test_plan;
418        change.test_environment = test_environment;
419        change.rollback_plan = rollback_plan;
420        change.impact_scope = impact_scope;
421        change.risk_level = risk_level;
422
423        let change_id = change.change_id.clone();
424        let mut changes = self.changes.write().await;
425        changes.insert(change_id, change.clone());
426
427        Ok(change)
428    }
429
430    /// Approve a change request
431    pub async fn approve_change(
432        &self,
433        change_id: &str,
434        approver: &str,
435        approver_id: Uuid,
436        comments: Option<String>,
437        conditions: Option<Vec<String>>,
438    ) -> Result<(), Error> {
439        let mut changes = self.changes.write().await;
440        let change = changes
441            .get_mut(change_id)
442            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
443
444        if change.status != ChangeStatus::PendingApproval {
445            return Err(Error::Generic("Change request is not pending approval".to_string()));
446        }
447
448        if !change.approvers.contains(&approver.to_string()) {
449            return Err(Error::Generic("User is not an approver for this change".to_string()));
450        }
451
452        change.approval_status.insert(approver.to_string(), ApprovalStatus::Approved);
453
454        let details = format!(
455            "Change approved by {}{}{}",
456            approver,
457            comments.map(|c| format!(" - {}", c)).unwrap_or_default(),
458            conditions
459                .map(|conds| format!(" - Conditions: {}", conds.join(", ")))
460                .unwrap_or_default()
461        );
462        change.add_history("approved".to_string(), approver_id, details);
463
464        // Check if all approvals are complete
465        if change.is_fully_approved() {
466            change.status = ChangeStatus::Approved;
467            change.add_history(
468                "all_approvals_complete".to_string(),
469                approver_id,
470                "All approvals received, change ready for implementation".to_string(),
471            );
472        }
473
474        Ok(())
475    }
476
477    /// Reject a change request
478    pub async fn reject_change(
479        &self,
480        change_id: &str,
481        approver: &str,
482        approver_id: Uuid,
483        reason: String,
484    ) -> Result<(), Error> {
485        let mut changes = self.changes.write().await;
486        let change = changes
487            .get_mut(change_id)
488            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
489
490        if change.status != ChangeStatus::PendingApproval {
491            return Err(Error::Generic("Change request is not pending approval".to_string()));
492        }
493
494        change.approval_status.insert(approver.to_string(), ApprovalStatus::Rejected);
495        change.status = ChangeStatus::Rejected;
496        change.add_history(
497            "rejected".to_string(),
498            approver_id,
499            format!("Change rejected: {}", reason),
500        );
501
502        Ok(())
503    }
504
505    /// Start change implementation
506    pub async fn start_implementation(
507        &self,
508        change_id: &str,
509        implementer_id: Uuid,
510        implementation_plan: String,
511        scheduled_time: Option<DateTime<Utc>>,
512    ) -> Result<(), Error> {
513        let mut changes = self.changes.write().await;
514        let change = changes
515            .get_mut(change_id)
516            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
517
518        if change.status != ChangeStatus::Approved {
519            return Err(Error::Generic(
520                "Change request must be approved before implementation".to_string(),
521            ));
522        }
523
524        change.status = ChangeStatus::Implementing;
525        change.implementation_plan = Some(implementation_plan);
526        change.scheduled_time = scheduled_time;
527        change.implementation_started = Some(Utc::now());
528
529        change.add_history(
530            "implementation_started".to_string(),
531            implementer_id,
532            "Change implementation started".to_string(),
533        );
534
535        Ok(())
536    }
537
538    /// Complete change implementation
539    pub async fn complete_change(
540        &self,
541        change_id: &str,
542        implementer_id: Uuid,
543        test_results: Option<String>,
544        post_implementation_review: Option<String>,
545    ) -> Result<(), Error> {
546        let mut changes = self.changes.write().await;
547        let change = changes
548            .get_mut(change_id)
549            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
550
551        if change.status != ChangeStatus::Implementing {
552            return Err(Error::Generic(
553                "Change request must be in implementing status".to_string(),
554            ));
555        }
556
557        change.status = ChangeStatus::Completed;
558        change.implementation_completed = Some(Utc::now());
559        change.test_results = test_results;
560        change.post_implementation_review = post_implementation_review;
561
562        change.add_history(
563            "completed".to_string(),
564            implementer_id,
565            "Change implementation completed".to_string(),
566        );
567
568        Ok(())
569    }
570
571    /// Get change request by ID
572    pub async fn get_change(&self, change_id: &str) -> Result<Option<ChangeRequest>, Error> {
573        let changes = self.changes.read().await;
574        Ok(changes.get(change_id).cloned())
575    }
576
577    /// Get all change requests
578    pub async fn get_all_changes(&self) -> Result<Vec<ChangeRequest>, Error> {
579        let changes = self.changes.read().await;
580        Ok(changes.values().cloned().collect())
581    }
582
583    /// Get changes by status
584    pub async fn get_changes_by_status(
585        &self,
586        status: ChangeStatus,
587    ) -> Result<Vec<ChangeRequest>, Error> {
588        let changes = self.changes.read().await;
589        Ok(changes.values().filter(|c| c.status == status).cloned().collect())
590    }
591
592    /// Get changes by requester
593    pub async fn get_changes_by_requester(
594        &self,
595        requester_id: Uuid,
596    ) -> Result<Vec<ChangeRequest>, Error> {
597        let changes = self.changes.read().await;
598        Ok(changes.values().filter(|c| c.requester_id == requester_id).cloned().collect())
599    }
600
601    /// Cancel a change request
602    pub async fn cancel_change(
603        &self,
604        change_id: &str,
605        user_id: Uuid,
606        reason: String,
607    ) -> Result<(), Error> {
608        let mut changes = self.changes.write().await;
609        let change = changes
610            .get_mut(change_id)
611            .ok_or_else(|| Error::Generic("Change request not found".to_string()))?;
612
613        change.status = ChangeStatus::Cancelled;
614        change.add_history(
615            "cancelled".to_string(),
616            user_id,
617            format!("Change cancelled: {}", reason),
618        );
619
620        Ok(())
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627
628    #[tokio::test]
629    async fn test_change_request_creation() {
630        let config = ChangeManagementConfig::default();
631        let engine = ChangeManagementEngine::new(config);
632
633        let change = engine
634            .create_change_request(
635                "Test Change".to_string(),
636                "Test description".to_string(),
637                Uuid::new_v4(),
638                ChangeType::Security,
639                ChangePriority::High,
640                ChangeUrgency::High,
641                vec!["system1".to_string()],
642                true,
643                Some("Test plan".to_string()),
644                Some("staging".to_string()),
645                None,
646                None,
647                None,
648            )
649            .await
650            .unwrap();
651
652        assert_eq!(change.status, ChangeStatus::PendingApproval);
653        assert!(!change.approvers.is_empty());
654    }
655}