1use axum::{
10 extract::{Path, State},
11 http::StatusCode,
12 response::Json,
13};
14use mockforge_core::security::{
15 access_review::{AccessReview, ReviewType, UserReviewItem},
16 access_review_service::AccessReviewService,
17 emit_security_event, EventActor, EventOutcome, EventTarget, SecurityEvent, SecurityEventType,
18};
19use serde::{Deserialize, Serialize};
20use std::sync::Arc;
21use tokio::sync::RwLock;
22use tracing::{error, info};
23use uuid::Uuid;
24
25use crate::handlers::auth_helpers::{extract_user_id_with_fallback, OptionalAuthClaims};
26
27#[derive(Clone)]
29pub struct AccessReviewState {
30 pub service: Arc<RwLock<AccessReviewService>>,
32}
33
34#[derive(Debug, Deserialize)]
36pub struct ApproveAccessRequest {
37 pub user_id: Uuid,
39 pub approved: bool,
41 pub justification: Option<String>,
43}
44
45#[derive(Debug, Deserialize)]
47pub struct RevokeAccessRequest {
48 pub user_id: Uuid,
50 pub reason: String,
52}
53
54#[derive(Debug, Deserialize)]
56pub struct UpdatePermissionsRequest {
57 pub user_id: Uuid,
59 pub roles: Vec<String>,
61 pub permissions: Vec<String>,
63 pub reason: Option<String>,
65}
66
67#[derive(Debug, Serialize)]
69pub struct ReviewListResponse {
70 pub reviews: Vec<ReviewSummary>,
72}
73
74#[derive(Debug, Serialize)]
76pub struct ReviewSummary {
77 pub review_id: String,
79 pub review_type: String,
81 pub status: String,
83 pub due_date: chrono::DateTime<chrono::Utc>,
85 pub items_count: u32,
87 pub pending_approvals: u32,
89}
90
91#[derive(Debug, Serialize)]
93pub struct ReviewDetailResponse {
94 #[serde(flatten)]
96 pub review: AccessReview,
97 pub items: Option<Vec<UserReviewItem>>,
99}
100
101#[derive(Debug, Serialize)]
103pub struct ReviewActionResponse {
104 pub review_id: String,
106 pub user_id: Uuid,
108 pub status: String,
110 pub timestamp: chrono::DateTime<chrono::Utc>,
112 pub message: Option<String>,
114}
115
116pub async fn list_reviews(
120 State(state): State<AccessReviewState>,
121) -> Result<Json<ReviewListResponse>, StatusCode> {
122 let service = state.service.read().await;
123 let reviews = service.get_all_reviews();
124
125 let summaries: Vec<ReviewSummary> = reviews
126 .iter()
127 .map(|review| ReviewSummary {
128 review_id: review.review_id.clone(),
129 review_type: format!("{:?}", review.review_type),
130 status: format!("{:?}", review.status),
131 due_date: review.due_date,
132 items_count: review.total_items,
133 pending_approvals: review.pending_approvals,
134 })
135 .collect();
136
137 Ok(Json(ReviewListResponse { reviews: summaries }))
138}
139
140pub async fn get_review(
144 State(state): State<AccessReviewState>,
145 Path(review_id): Path<String>,
146) -> Result<Json<ReviewDetailResponse>, StatusCode> {
147 let service = state.service.read().await;
148
149 let review = service
150 .get_review(&review_id)
151 .ok_or_else(|| {
152 error!("Review {} not found", review_id);
153 StatusCode::NOT_FOUND
154 })?
155 .clone();
156
157 let items = service
159 .engine()
160 .get_review_items(&review_id)
161 .map(|items_map| items_map.values().cloned().collect());
162
163 Ok(Json(ReviewDetailResponse { review, items }))
164}
165
166pub async fn approve_access(
170 State(state): State<AccessReviewState>,
171 Path(review_id): Path<String>,
172 claims: OptionalAuthClaims,
173 Json(request): Json<ApproveAccessRequest>,
174) -> Result<Json<ReviewActionResponse>, StatusCode> {
175 let mut service = state.service.write().await;
176
177 let approver_id = extract_user_id_with_fallback(&claims);
179
180 match service
181 .approve_user_access(&review_id, request.user_id, approver_id, request.justification)
182 .await
183 {
184 Ok(()) => {
185 info!("Access approved for user {} in review {}", request.user_id, review_id);
186
187 let event = SecurityEvent::new(SecurityEventType::AuthzAccessGranted, None, None)
189 .with_actor(EventActor {
190 user_id: Some(approver_id.to_string()),
191 username: None,
192 ip_address: None,
193 user_agent: None,
194 })
195 .with_target(EventTarget {
196 resource_type: Some("access_review".to_string()),
197 resource_id: Some(review_id.clone()),
198 method: None,
199 })
200 .with_outcome(EventOutcome {
201 success: true,
202 reason: Some("Access approved in review".to_string()),
203 })
204 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id));
205 emit_security_event(event).await;
206
207 Ok(Json(ReviewActionResponse {
208 review_id,
209 user_id: request.user_id,
210 status: "approved".to_string(),
211 timestamp: chrono::Utc::now(),
212 message: Some("Access approved successfully".to_string()),
213 }))
214 }
215 Err(e) => {
216 error!("Failed to approve access: {}", e);
217 Err(StatusCode::BAD_REQUEST)
218 }
219 }
220}
221
222pub async fn revoke_access(
226 State(state): State<AccessReviewState>,
227 Path(review_id): Path<String>,
228 claims: OptionalAuthClaims,
229 Json(request): Json<RevokeAccessRequest>,
230) -> Result<Json<ReviewActionResponse>, StatusCode> {
231 let mut service = state.service.write().await;
232
233 let revoker_id = extract_user_id_with_fallback(&claims);
235
236 match service
237 .revoke_user_access(&review_id, request.user_id, revoker_id, request.reason.clone())
238 .await
239 {
240 Ok(()) => {
241 info!("Access revoked for user {} in review {}", request.user_id, review_id);
242
243 let event = SecurityEvent::new(SecurityEventType::AccessUserSuspended, None, None)
245 .with_actor(EventActor {
246 user_id: Some(revoker_id.to_string()),
247 username: None,
248 ip_address: None,
249 user_agent: None,
250 })
251 .with_target(EventTarget {
252 resource_type: Some("access_review".to_string()),
253 resource_id: Some(review_id.clone()),
254 method: None,
255 })
256 .with_outcome(EventOutcome {
257 success: true,
258 reason: Some(request.reason.clone()),
259 })
260 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
261 .with_metadata("review_id".to_string(), serde_json::json!(review_id));
262 emit_security_event(event).await;
263
264 Ok(Json(ReviewActionResponse {
265 review_id,
266 user_id: request.user_id,
267 status: "revoked".to_string(),
268 timestamp: chrono::Utc::now(),
269 message: Some(format!("Access revoked: {}", request.reason)),
270 }))
271 }
272 Err(e) => {
273 error!("Failed to revoke access: {}", e);
274 Err(StatusCode::BAD_REQUEST)
275 }
276 }
277}
278
279pub async fn update_permissions(
283 State(state): State<AccessReviewState>,
284 Path(review_id): Path<String>,
285 claims: OptionalAuthClaims,
286 Json(request): Json<UpdatePermissionsRequest>,
287) -> Result<Json<ReviewActionResponse>, StatusCode> {
288 let mut service = state.service.write().await;
289
290 let updater_id = extract_user_id_with_fallback(&claims);
292
293 match service
294 .update_user_permissions(
295 &review_id,
296 request.user_id,
297 updater_id,
298 request.roles.clone(),
299 request.permissions.clone(),
300 request.reason.clone(),
301 )
302 .await
303 {
304 Ok(()) => {
305 info!("Permissions updated for user {} in review {}", request.user_id, review_id);
306
307 let event = SecurityEvent::new(SecurityEventType::AuthzPermissionChanged, None, None)
309 .with_actor(EventActor {
310 user_id: Some(updater_id.to_string()),
311 username: None,
312 ip_address: None,
313 user_agent: None,
314 })
315 .with_target(EventTarget {
316 resource_type: Some("access_review".to_string()),
317 resource_id: Some(review_id.clone()),
318 method: None,
319 })
320 .with_outcome(EventOutcome {
321 success: true,
322 reason: request.reason.clone(),
323 })
324 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
325 .with_metadata("review_id".to_string(), serde_json::json!(review_id))
326 .with_metadata("new_roles".to_string(), serde_json::json!(request.roles))
327 .with_metadata(
328 "new_permissions".to_string(),
329 serde_json::json!(request.permissions),
330 );
331 emit_security_event(event).await;
332
333 Ok(Json(ReviewActionResponse {
334 review_id,
335 user_id: request.user_id,
336 status: "permissions_updated".to_string(),
337 timestamp: chrono::Utc::now(),
338 message: Some("Permissions updated successfully".to_string()),
339 }))
340 }
341 Err(e) => {
342 error!("Failed to update permissions: {}", e);
343 Err(StatusCode::BAD_REQUEST)
344 }
345 }
346}
347
348pub async fn get_review_report(
352 State(state): State<AccessReviewState>,
353 Path(review_id): Path<String>,
354) -> Result<Json<serde_json::Value>, StatusCode> {
355 let service = state.service.read().await;
356
357 let review = service.get_review(&review_id).ok_or_else(|| {
358 error!("Review {} not found", review_id);
359 StatusCode::NOT_FOUND
360 })?;
361
362 let report = serde_json::json!({
364 "review_id": review.review_id,
365 "review_date": review.review_date,
366 "review_type": format!("{:?}", review.review_type),
367 "status": format!("{:?}", review.status),
368 "total_items": review.total_items,
369 "items_reviewed": review.items_reviewed,
370 "findings": review.findings,
371 "actions_taken": review.actions_taken,
372 "pending_reviews": review.pending_approvals,
373 "next_review_date": review.next_review_date,
374 });
375
376 Ok(Json(report))
377}
378
379pub async fn start_review(
383 State(state): State<AccessReviewState>,
384 Json(request): Json<StartReviewRequest>,
385) -> Result<Json<ReviewDetailResponse>, StatusCode> {
386 let mut service = state.service.write().await;
387
388 let review_id = match request.review_type {
390 ReviewType::UserAccess => service.start_user_access_review().await.map_err(|e| {
391 error!("Failed to start user access review: {}", e);
392 StatusCode::INTERNAL_SERVER_ERROR
393 })?,
394 ReviewType::PrivilegedAccess => {
395 service.start_privileged_access_review().await.map_err(|e| {
396 error!("Failed to start privileged access review: {}", e);
397 StatusCode::INTERNAL_SERVER_ERROR
398 })?
399 }
400 ReviewType::ApiToken => service.start_token_review().await.map_err(|e| {
401 error!("Failed to start token review: {}", e);
402 StatusCode::INTERNAL_SERVER_ERROR
403 })?,
404 ReviewType::ResourceAccess => {
405 return Err(StatusCode::NOT_IMPLEMENTED);
406 }
407 };
408
409 info!("Started access review: {}", review_id);
410
411 let review = service
413 .get_review(&review_id)
414 .ok_or_else(|| {
415 error!("Review {} not found after creation", review_id);
416 StatusCode::INTERNAL_SERVER_ERROR
417 })?
418 .clone();
419
420 let event = SecurityEvent::new(SecurityEventType::ComplianceComplianceCheck, None, None)
422 .with_target(EventTarget {
423 resource_type: Some("access_review".to_string()),
424 resource_id: Some(review_id.clone()),
425 method: None,
426 })
427 .with_outcome(EventOutcome {
428 success: true,
429 reason: Some("Access review started".to_string()),
430 });
431 emit_security_event(event).await;
432
433 let items = service
434 .engine()
435 .get_review_items(&review_id)
436 .map(|items_map| items_map.values().cloned().collect());
437
438 Ok(Json(ReviewDetailResponse { review, items }))
439}
440
441#[derive(Debug, Deserialize)]
443pub struct StartReviewRequest {
444 pub review_type: ReviewType,
446}
447
448pub fn access_review_router(state: AccessReviewState) -> axum::Router {
450 use axum::routing::{get, post};
451
452 axum::Router::new()
453 .route("/", get(list_reviews))
454 .route("/start", post(start_review))
455 .route("/{review_id}", get(get_review))
456 .route("/{review_id}/approve", post(approve_access))
457 .route("/{review_id}/revoke", post(revoke_access))
458 .route("/{review_id}/update-permissions", post(update_permissions))
459 .route("/{review_id}/report", get(get_review_report))
460 .with_state(state)
461}