1use 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#[derive(Clone)]
29pub struct ChangeManagementState {
30 pub engine: Arc<RwLock<ChangeManagementEngine>>,
32}
33
34#[derive(Debug, Deserialize)]
36pub struct CreateChangeRequest {
37 pub title: String,
39 pub description: String,
41 pub change_type: ChangeType,
43 pub priority: ChangePriority,
45 pub urgency: ChangeUrgency,
47 pub affected_systems: Vec<String>,
49 pub impact_scope: Option<String>,
51 pub risk_level: Option<String>,
53 pub rollback_plan: Option<String>,
55 pub testing_required: bool,
57 pub test_plan: Option<String>,
59 pub test_environment: Option<String>,
61}
62
63#[derive(Debug, Deserialize)]
65pub struct ApproveChangeRequest {
66 pub approved: bool,
68 pub comments: Option<String>,
70 pub conditions: Option<Vec<String>>,
72 pub reason: Option<String>,
74}
75
76#[derive(Debug, Deserialize)]
78pub struct StartImplementationRequest {
79 pub implementation_plan: String,
81 pub scheduled_time: Option<chrono::DateTime<chrono::Utc>>,
83}
84
85#[derive(Debug, Deserialize)]
87pub struct CompleteChangeRequest {
88 pub test_results: Option<String>,
90 pub post_implementation_review: Option<String>,
92}
93
94#[derive(Debug, Serialize)]
96pub struct ChangeRequestResponse {
97 pub change_id: String,
99 pub status: ChangeStatus,
101 pub approvers: Vec<String>,
103 pub request_date: chrono::DateTime<chrono::Utc>,
105}
106
107#[derive(Debug, Serialize)]
109pub struct ChangeListResponse {
110 pub changes: Vec<ChangeSummary>,
112}
113
114#[derive(Debug, Serialize)]
116pub struct ChangeSummary {
117 pub change_id: String,
119 pub title: String,
121 pub status: ChangeStatus,
123 pub priority: ChangePriority,
125 pub request_date: chrono::DateTime<chrono::Utc>,
127}
128
129pub 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 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 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
192pub 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 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 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 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
280pub 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 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 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
328pub 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 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 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
376pub 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
399pub 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 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
464pub 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
490pub 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}