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