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