Skip to main content

mockforge_http/handlers/
change_management.rs

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