1use 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#[derive(Clone)]
35pub struct ChangeManagementState {
36 pub engine: Arc<RwLock<ChangeManagementEngine>>,
38}
39
40#[derive(Debug, Deserialize)]
42pub struct CreateChangeRequest {
43 pub title: String,
45 pub description: String,
47 pub change_type: ChangeType,
49 pub priority: ChangePriority,
51 pub urgency: ChangeUrgency,
53 pub affected_systems: Vec<String>,
55 pub impact_scope: Option<String>,
57 pub risk_level: Option<String>,
59 pub rollback_plan: Option<String>,
61 pub testing_required: bool,
63 pub test_plan: Option<String>,
65 pub test_environment: Option<String>,
67}
68
69#[derive(Debug, Deserialize)]
71pub struct ApproveChangeRequest {
72 pub approved: bool,
74 pub comments: Option<String>,
76 pub conditions: Option<Vec<String>>,
78 pub reason: Option<String>,
80}
81
82#[derive(Debug, Deserialize)]
84pub struct StartImplementationRequest {
85 pub implementation_plan: String,
87 pub scheduled_time: Option<chrono::DateTime<chrono::Utc>>,
89}
90
91#[derive(Debug, Deserialize)]
93pub struct CompleteChangeRequest {
94 pub test_results: Option<String>,
96 pub post_implementation_review: Option<String>,
98}
99
100#[derive(Debug, Serialize)]
102pub struct ChangeRequestResponse {
103 pub change_id: String,
105 pub status: ChangeStatus,
107 pub approvers: Vec<String>,
109 pub request_date: chrono::DateTime<chrono::Utc>,
111}
112
113#[derive(Debug, Serialize)]
115pub struct ChangeListResponse {
116 pub changes: Vec<ChangeSummary>,
118}
119
120#[derive(Debug, Serialize)]
122pub struct ChangeSummary {
123 pub change_id: String,
125 pub title: String,
127 pub status: ChangeStatus,
129 pub priority: ChangePriority,
131 pub request_date: chrono::DateTime<chrono::Utc>,
133}
134
135pub async fn create_change_request(
139 State(state): State<ChangeManagementState>,
140 claims: OptionalAuthClaims,
141 Json(request): Json<CreateChangeRequest>,
142) -> Result<Json<ChangeRequestResponse>, StatusCode> {
143 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 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
198pub 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 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 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 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
292pub 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 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 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
345pub 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 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 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
398pub 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
421pub 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 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
473pub 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
499pub 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}