mockforge_http/handlers/
change_management.rs

1//! HTTP handlers for change management
2//!
3//! This module provides REST API endpoints for managing change requests,
4//! approvals, implementation tracking, and completion.
5
6use axum::{
7    extract::{Extension, Path, Query, State},
8    http::StatusCode,
9    response::Json,
10};
11use mockforge_core::security::{
12    change_management::{
13        ChangeManagementEngine, ChangePriority, ChangeStatus, ChangeType, ChangeUrgency,
14    },
15    emit_security_event, EventActor, EventOutcome, EventTarget, SecurityEvent, SecurityEventType,
16};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::sync::Arc;
20use tokio::sync::RwLock;
21use tracing::{error, info};
22use uuid::Uuid;
23
24use crate::auth::types::AuthClaims;
25use crate::handlers::auth_helpers::{extract_user_id_with_fallback, extract_username_from_claims};
26
27/// State for change management handlers
28#[derive(Clone)]
29pub struct ChangeManagementState {
30    /// Change management engine
31    pub engine: Arc<RwLock<ChangeManagementEngine>>,
32}
33
34/// Request to create a change request
35#[derive(Debug, Deserialize)]
36pub struct CreateChangeRequest {
37    /// Change title
38    pub title: String,
39    /// Change description
40    pub description: String,
41    /// Change type
42    pub change_type: ChangeType,
43    /// Change priority
44    pub priority: ChangePriority,
45    /// Change urgency
46    pub urgency: ChangeUrgency,
47    /// Affected systems
48    pub affected_systems: Vec<String>,
49    /// Impact scope
50    pub impact_scope: Option<String>,
51    /// Risk level
52    pub risk_level: Option<String>,
53    /// Rollback plan
54    pub rollback_plan: Option<String>,
55    /// Testing required
56    pub testing_required: bool,
57    /// Test plan
58    pub test_plan: Option<String>,
59    /// Test environment
60    pub test_environment: Option<String>,
61}
62
63/// Request to approve a change
64#[derive(Debug, Deserialize)]
65pub struct ApproveChangeRequest {
66    /// Whether to approve
67    pub approved: bool,
68    /// Comments
69    pub comments: Option<String>,
70    /// Conditions (if approved)
71    pub conditions: Option<Vec<String>>,
72    /// Rejection reason (if rejected)
73    pub reason: Option<String>,
74}
75
76/// Request to start implementation
77#[derive(Debug, Deserialize)]
78pub struct StartImplementationRequest {
79    /// Implementation plan
80    pub implementation_plan: String,
81    /// Scheduled time (optional)
82    pub scheduled_time: Option<chrono::DateTime<chrono::Utc>>,
83}
84
85/// Request to complete change
86#[derive(Debug, Deserialize)]
87pub struct CompleteChangeRequest {
88    /// Test results
89    pub test_results: Option<String>,
90    /// Post-implementation review
91    pub post_implementation_review: Option<String>,
92}
93
94/// Response for change request
95#[derive(Debug, Serialize)]
96pub struct ChangeRequestResponse {
97    /// Change ID
98    pub change_id: String,
99    /// Status
100    pub status: ChangeStatus,
101    /// Approvers
102    pub approvers: Vec<String>,
103    /// Request date
104    pub request_date: chrono::DateTime<chrono::Utc>,
105}
106
107/// Response for change list
108#[derive(Debug, Serialize)]
109pub struct ChangeListResponse {
110    /// Changes
111    pub changes: Vec<ChangeSummary>,
112}
113
114/// Summary of a change request
115#[derive(Debug, Serialize)]
116pub struct ChangeSummary {
117    /// Change ID
118    pub change_id: String,
119    /// Title
120    pub title: String,
121    /// Status
122    pub status: ChangeStatus,
123    /// Priority
124    pub priority: ChangePriority,
125    /// Request date
126    pub request_date: chrono::DateTime<chrono::Utc>,
127}
128
129/// Create a change request
130///
131/// POST /api/v1/change-management/change-requests
132pub async fn create_change_request(
133    State(state): State<ChangeManagementState>,
134    Json(request): Json<CreateChangeRequest>,
135    claims: Option<Extension<AuthClaims>>,
136) -> Result<Json<ChangeRequestResponse>, StatusCode> {
137    // Extract requester ID from authentication claims, or use default for mock server
138    let requester_id = extract_user_id_with_fallback(claims);
139
140    let engine = state.engine.write().await;
141    let change = engine
142        .create_change_request(
143            request.title,
144            request.description,
145            requester_id,
146            request.change_type,
147            request.priority,
148            request.urgency,
149            request.affected_systems,
150            request.testing_required,
151            request.test_plan,
152            request.test_environment,
153            request.rollback_plan,
154            request.impact_scope,
155            request.risk_level,
156        )
157        .await
158        .map_err(|e| {
159            error!("Failed to create change request: {}", e);
160            StatusCode::INTERNAL_SERVER_ERROR
161        })?;
162
163    info!("Change request created: {}", change.change_id);
164
165    // Emit security event
166    let event = SecurityEvent::new(SecurityEventType::ConfigChanged, None, None)
167        .with_actor(EventActor {
168            user_id: Some(requester_id.to_string()),
169            username: None,
170            ip_address: None,
171            user_agent: None,
172        })
173        .with_target(EventTarget {
174            resource_type: Some("change_request".to_string()),
175            resource_id: Some(change.change_id.clone()),
176            method: None,
177        })
178        .with_outcome(EventOutcome {
179            success: true,
180            reason: Some("Change request created".to_string()),
181        });
182    emit_security_event(event).await;
183
184    Ok(Json(ChangeRequestResponse {
185        change_id: change.change_id,
186        status: change.status,
187        approvers: change.approvers,
188        request_date: change.request_date,
189    }))
190}
191
192/// Approve a change request
193///
194/// POST /api/v1/change-management/change-requests/{change_id}/approve
195pub async fn approve_change(
196    State(state): State<ChangeManagementState>,
197    Path(change_id): Path<String>,
198    Json(request): Json<ApproveChangeRequest>,
199    claims: Option<Extension<AuthClaims>>,
200) -> Result<Json<serde_json::Value>, StatusCode> {
201    // Extract approver ID and name from authentication claims, or use defaults for mock server
202    let approver_id = extract_user_id_with_fallback(claims.clone());
203    let approver = extract_username_from_claims(claims)
204        .unwrap_or_else(|| format!("user-{}", approver_id));
205
206    let engine = state.engine.write().await;
207
208    if request.approved {
209        engine
210            .approve_change(&change_id, &approver, approver_id, request.comments, request.conditions)
211            .await
212            .map_err(|e| {
213                error!("Failed to approve change: {}", e);
214                StatusCode::BAD_REQUEST
215            })?;
216
217        info!("Change request approved: {}", change_id);
218
219        // Emit security event
220        let event = SecurityEvent::new(SecurityEventType::ConfigChanged, None, None)
221            .with_actor(EventActor {
222                user_id: Some(approver_id.to_string()),
223                username: None,
224                ip_address: None,
225                user_agent: None,
226            })
227            .with_target(EventTarget {
228                resource_type: Some("change_request".to_string()),
229                resource_id: Some(change_id.clone()),
230                method: None,
231            })
232            .with_outcome(EventOutcome {
233                success: true,
234                reason: Some("Change approved".to_string()),
235            });
236        emit_security_event(event).await;
237
238        Ok(Json(serde_json::json!({
239            "status": "approved",
240            "change_id": change_id
241        })))
242    } else {
243        let reason = request.reason.unwrap_or_else(|| "No reason provided".to_string());
244        engine
245            .reject_change(&change_id, &approver, approver_id, reason.clone())
246            .await
247            .map_err(|e| {
248                error!("Failed to reject change: {}", e);
249                StatusCode::BAD_REQUEST
250            })?;
251
252        info!("Change request rejected: {}", change_id);
253
254        // Emit security event
255        let event = SecurityEvent::new(SecurityEventType::ConfigChanged, None, None)
256            .with_actor(EventActor {
257                user_id: Some(approver_id.to_string()),
258                username: None,
259                ip_address: None,
260                user_agent: None,
261            })
262            .with_target(EventTarget {
263                resource_type: Some("change_request".to_string()),
264                resource_id: Some(change_id.clone()),
265                method: None,
266            })
267            .with_outcome(EventOutcome {
268                success: false,
269                reason: Some(format!("Change rejected: {}", reason)),
270            });
271        emit_security_event(event).await;
272
273        Ok(Json(serde_json::json!({
274            "status": "rejected",
275            "change_id": change_id
276        })))
277    }
278}
279
280/// Start change implementation
281///
282/// POST /api/v1/change-management/change-requests/{change_id}/implement
283pub async fn start_implementation(
284    State(state): State<ChangeManagementState>,
285    Path(change_id): Path<String>,
286    Json(request): Json<StartImplementationRequest>,
287    claims: Option<Extension<AuthClaims>>,
288) -> Result<Json<serde_json::Value>, StatusCode> {
289    // Extract implementer ID from authentication claims, or use default for mock server
290    let implementer_id = extract_user_id_with_fallback(claims);
291
292    let engine = state.engine.write().await;
293    engine
294        .start_implementation(&change_id, implementer_id, request.implementation_plan, request.scheduled_time)
295        .await
296        .map_err(|e| {
297            error!("Failed to start implementation: {}", e);
298            StatusCode::BAD_REQUEST
299        })?;
300
301    info!("Change implementation started: {}", change_id);
302
303    // Emit security event
304    let event = SecurityEvent::new(SecurityEventType::ConfigChanged, None, None)
305        .with_actor(EventActor {
306            user_id: Some(implementer_id.to_string()),
307            username: None,
308            ip_address: None,
309            user_agent: None,
310        })
311        .with_target(EventTarget {
312            resource_type: Some("change_request".to_string()),
313            resource_id: Some(change_id.clone()),
314            method: None,
315        })
316        .with_outcome(EventOutcome {
317            success: true,
318            reason: Some("Change implementation started".to_string()),
319        });
320    emit_security_event(event).await;
321
322    Ok(Json(serde_json::json!({
323        "status": "implementing",
324        "change_id": change_id
325    })))
326}
327
328/// Complete change implementation
329///
330/// POST /api/v1/change-management/change-requests/{change_id}/complete
331pub async fn complete_change(
332    State(state): State<ChangeManagementState>,
333    Path(change_id): Path<String>,
334    Json(request): Json<CompleteChangeRequest>,
335    claims: Option<Extension<AuthClaims>>,
336) -> Result<Json<serde_json::Value>, StatusCode> {
337    // Extract implementer ID from authentication claims, or use default for mock server
338    let implementer_id = extract_user_id_with_fallback(claims);
339
340    let engine = state.engine.write().await;
341    engine
342        .complete_change(&change_id, implementer_id, request.test_results, request.post_implementation_review)
343        .await
344        .map_err(|e| {
345            error!("Failed to complete change: {}", e);
346            StatusCode::BAD_REQUEST
347        })?;
348
349    info!("Change implementation completed: {}", change_id);
350
351    // Emit security event
352    let event = SecurityEvent::new(SecurityEventType::ConfigChanged, None, None)
353        .with_actor(EventActor {
354            user_id: Some(implementer_id.to_string()),
355            username: None,
356            ip_address: None,
357            user_agent: None,
358        })
359        .with_target(EventTarget {
360            resource_type: Some("change_request".to_string()),
361            resource_id: Some(change_id.clone()),
362            method: None,
363        })
364        .with_outcome(EventOutcome {
365            success: true,
366            reason: Some("Change implementation completed".to_string()),
367        });
368    emit_security_event(event).await;
369
370    Ok(Json(serde_json::json!({
371        "status": "completed",
372        "change_id": change_id
373    })))
374}
375
376/// Get a change request
377///
378/// GET /api/v1/change-management/change-requests/{change_id}
379pub async fn get_change(
380    State(state): State<ChangeManagementState>,
381    Path(change_id): Path<String>,
382) -> Result<Json<serde_json::Value>, StatusCode> {
383    let engine = state.engine.read().await;
384    let change = engine
385        .get_change(&change_id)
386        .await
387        .map_err(|e| {
388            error!("Failed to get change: {}", e);
389            StatusCode::INTERNAL_SERVER_ERROR
390        })?
391        .ok_or_else(|| {
392            error!("Change request not found: {}", change_id);
393            StatusCode::NOT_FOUND
394        })?;
395
396    Ok(Json(serde_json::to_value(&change).unwrap()))
397}
398
399/// List change requests
400///
401/// GET /api/v1/change-management/change-requests
402pub async fn list_changes(
403    State(state): State<ChangeManagementState>,
404    Query(params): Query<HashMap<String, String>>,
405) -> Result<Json<ChangeListResponse>, StatusCode> {
406    let engine = state.engine.read().await;
407
408    let changes = if let Some(status_str) = params.get("status") {
409        // Parse status from query parameter
410        let status = match status_str.as_str() {
411            "pending_approval" => ChangeStatus::PendingApproval,
412            "approved" => ChangeStatus::Approved,
413            "rejected" => ChangeStatus::Rejected,
414            "implementing" => ChangeStatus::Implementing,
415            "completed" => ChangeStatus::Completed,
416            "cancelled" => ChangeStatus::Cancelled,
417            "rolled_back" => ChangeStatus::RolledBack,
418            _ => return Err(StatusCode::BAD_REQUEST),
419        };
420        engine
421            .get_changes_by_status(status)
422            .await
423            .map_err(|e| {
424                error!("Failed to get changes by status: {}", e);
425                StatusCode::INTERNAL_SERVER_ERROR
426            })?
427    } else if let Some(requester_str) = params.get("requester_id") {
428        let requester_id = requester_str
429            .parse::<Uuid>()
430            .map_err(|_| StatusCode::BAD_REQUEST)?;
431        engine
432            .get_changes_by_requester(requester_id)
433            .await
434            .map_err(|e| {
435                error!("Failed to get changes by requester: {}", e);
436                StatusCode::INTERNAL_SERVER_ERROR
437            })?
438    } else {
439        engine
440            .get_all_changes()
441            .await
442            .map_err(|e| {
443                error!("Failed to get all changes: {}", e);
444                StatusCode::INTERNAL_SERVER_ERROR
445            })?
446    };
447
448    let summaries: Vec<ChangeSummary> = changes
449        .into_iter()
450        .map(|c| ChangeSummary {
451            change_id: c.change_id,
452            title: c.title,
453            status: c.status,
454            priority: c.priority,
455            request_date: c.request_date,
456        })
457        .collect();
458
459    Ok(Json(ChangeListResponse {
460        changes: summaries,
461    }))
462}
463
464/// Get change history
465///
466/// GET /api/v1/change-management/change-requests/{change_id}/history
467pub async fn get_change_history(
468    State(state): State<ChangeManagementState>,
469    Path(change_id): Path<String>,
470) -> Result<Json<serde_json::Value>, StatusCode> {
471    let engine = state.engine.read().await;
472    let change = engine
473        .get_change(&change_id)
474        .await
475        .map_err(|e| {
476            error!("Failed to get change: {}", e);
477            StatusCode::INTERNAL_SERVER_ERROR
478        })?
479        .ok_or_else(|| {
480            error!("Change request not found: {}", change_id);
481            StatusCode::NOT_FOUND
482        })?;
483
484    Ok(Json(serde_json::json!({
485        "change_id": change.change_id,
486        "history": change.history
487    })))
488}
489
490/// Create change management router
491pub fn change_management_router(state: ChangeManagementState) -> axum::Router {
492    use axum::routing::{get, post};
493
494    axum::Router::new()
495        .route("/change-requests", get(list_changes))
496        .route("/change-requests", post(create_change_request))
497        .route("/change-requests/{change_id}", get(get_change))
498        .route("/change-requests/{change_id}/approve", post(approve_change))
499        .route("/change-requests/{change_id}/implement", post(start_implementation))
500        .route("/change-requests/{change_id}/complete", post(complete_change))
501        .route("/change-requests/{change_id}/history", get(get_change_history))
502        .with_state(state)
503}