1use 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#[derive(Clone)]
30pub struct ChangeManagementState {
31 pub engine: Arc<RwLock<ChangeManagementEngine>>,
33}
34
35#[derive(Debug, Deserialize)]
37pub struct CreateChangeRequest {
38 pub title: String,
40 pub description: String,
42 pub change_type: ChangeType,
44 pub priority: ChangePriority,
46 pub urgency: ChangeUrgency,
48 pub affected_systems: Vec<String>,
50 pub impact_scope: Option<String>,
52 pub risk_level: Option<String>,
54 pub rollback_plan: Option<String>,
56 pub testing_required: bool,
58 pub test_plan: Option<String>,
60 pub test_environment: Option<String>,
62}
63
64#[derive(Debug, Deserialize)]
66pub struct ApproveChangeRequest {
67 pub approved: bool,
69 pub comments: Option<String>,
71 pub conditions: Option<Vec<String>>,
73 pub reason: Option<String>,
75}
76
77#[derive(Debug, Deserialize)]
79pub struct StartImplementationRequest {
80 pub implementation_plan: String,
82 pub scheduled_time: Option<chrono::DateTime<chrono::Utc>>,
84}
85
86#[derive(Debug, Deserialize)]
88pub struct CompleteChangeRequest {
89 pub test_results: Option<String>,
91 pub post_implementation_review: Option<String>,
93}
94
95#[derive(Debug, Serialize)]
97pub struct ChangeRequestResponse {
98 pub change_id: String,
100 pub status: ChangeStatus,
102 pub approvers: Vec<String>,
104 pub request_date: chrono::DateTime<chrono::Utc>,
106}
107
108#[derive(Debug, Serialize)]
110pub struct ChangeListResponse {
111 pub changes: Vec<ChangeSummary>,
113}
114
115#[derive(Debug, Serialize)]
117pub struct ChangeSummary {
118 pub change_id: String,
120 pub title: String,
122 pub status: ChangeStatus,
124 pub priority: ChangePriority,
126 pub request_date: chrono::DateTime<chrono::Utc>,
128}
129
130pub async fn create_change_request(
134 State(state): State<ChangeManagementState>,
135 claims: OptionalAuthClaims,
136 Json(request): Json<CreateChangeRequest>,
137) -> Result<Json<ChangeRequestResponse>, StatusCode> {
138 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 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
193pub 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 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 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 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
287pub 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 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 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
340pub 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 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 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
393pub 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
416pub 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 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
468pub 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
494pub 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}