1use axum::{
15 extract::{Path, State},
16 http::StatusCode,
17 response::Json,
18};
19use mockforge_core::security::{
20 access_review::{AccessReview, ReviewType, UserReviewItem},
21 access_review_service::AccessReviewService,
22 emit_security_event, EventActor, EventOutcome, EventTarget, SecurityEvent, SecurityEventType,
23};
24use serde::{Deserialize, Serialize};
25use std::sync::Arc;
26use tokio::sync::RwLock;
27use tracing::{error, info};
28use uuid::Uuid;
29
30use crate::handlers::auth_helpers::{extract_user_id_with_fallback, OptionalAuthClaims};
31
32#[derive(Clone)]
34pub struct AccessReviewState {
35 pub service: Arc<RwLock<AccessReviewService>>,
37}
38
39#[derive(Debug, Deserialize)]
41pub struct ApproveAccessRequest {
42 pub user_id: Uuid,
44 pub approved: bool,
46 pub justification: Option<String>,
48}
49
50#[derive(Debug, Deserialize)]
52pub struct RevokeAccessRequest {
53 pub user_id: Uuid,
55 pub reason: String,
57}
58
59#[derive(Debug, Deserialize)]
61pub struct UpdatePermissionsRequest {
62 pub user_id: Uuid,
64 pub roles: Vec<String>,
66 pub permissions: Vec<String>,
68 pub reason: Option<String>,
70}
71
72#[derive(Debug, Serialize)]
74pub struct ReviewListResponse {
75 pub reviews: Vec<ReviewSummary>,
77}
78
79#[derive(Debug, Serialize)]
81pub struct ReviewSummary {
82 pub review_id: String,
84 pub review_type: String,
86 pub status: String,
88 pub due_date: chrono::DateTime<chrono::Utc>,
90 pub items_count: u32,
92 pub pending_approvals: u32,
94}
95
96#[derive(Debug, Serialize)]
98pub struct ReviewDetailResponse {
99 #[serde(flatten)]
101 pub review: AccessReview,
102 pub items: Option<Vec<UserReviewItem>>,
104}
105
106#[derive(Debug, Serialize)]
108pub struct ReviewActionResponse {
109 pub review_id: String,
111 pub user_id: Uuid,
113 pub status: String,
115 pub timestamp: chrono::DateTime<chrono::Utc>,
117 pub message: Option<String>,
119}
120
121pub async fn list_reviews(
125 State(state): State<AccessReviewState>,
126) -> Result<Json<ReviewListResponse>, StatusCode> {
127 let service = state.service.read().await;
128 let reviews = service.get_all_reviews();
129
130 let summaries: Vec<ReviewSummary> = reviews
131 .iter()
132 .map(|review| ReviewSummary {
133 review_id: review.review_id.clone(),
134 review_type: format!("{:?}", review.review_type),
135 status: format!("{:?}", review.status),
136 due_date: review.due_date,
137 items_count: review.total_items,
138 pending_approvals: review.pending_approvals,
139 })
140 .collect();
141
142 Ok(Json(ReviewListResponse { reviews: summaries }))
143}
144
145pub async fn get_review(
149 State(state): State<AccessReviewState>,
150 Path(review_id): Path<String>,
151) -> Result<Json<ReviewDetailResponse>, StatusCode> {
152 let service = state.service.read().await;
153
154 let review = service
155 .get_review(&review_id)
156 .ok_or_else(|| {
157 error!("Review {} not found", review_id);
158 StatusCode::NOT_FOUND
159 })?
160 .clone();
161
162 let items = service
164 .engine()
165 .get_review_items(&review_id)
166 .map(|items_map| items_map.values().cloned().collect());
167
168 Ok(Json(ReviewDetailResponse { review, items }))
169}
170
171pub async fn approve_access(
175 State(state): State<AccessReviewState>,
176 Path(review_id): Path<String>,
177 claims: OptionalAuthClaims,
178 Json(request): Json<ApproveAccessRequest>,
179) -> Result<Json<ReviewActionResponse>, StatusCode> {
180 let mut service = state.service.write().await;
181
182 let approver_id = extract_user_id_with_fallback(&claims);
184
185 match service
186 .approve_user_access(&review_id, request.user_id, approver_id, request.justification)
187 .await
188 {
189 Ok(()) => {
190 info!("Access approved for user {} in review {}", request.user_id, review_id);
191
192 let event = SecurityEvent::new(SecurityEventType::AuthzAccessGranted, None, None)
194 .with_actor(EventActor {
195 user_id: Some(approver_id.to_string()),
196 username: None,
197 ip_address: None,
198 user_agent: None,
199 })
200 .with_target(EventTarget {
201 resource_type: Some("access_review".to_string()),
202 resource_id: Some(review_id.clone()),
203 method: None,
204 })
205 .with_outcome(EventOutcome {
206 success: true,
207 reason: Some("Access approved in review".to_string()),
208 })
209 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id));
210 emit_security_event(event).await;
211
212 Ok(Json(ReviewActionResponse {
213 review_id,
214 user_id: request.user_id,
215 status: "approved".to_string(),
216 timestamp: chrono::Utc::now(),
217 message: Some("Access approved successfully".to_string()),
218 }))
219 }
220 Err(e) => {
221 error!("Failed to approve access: {}", e);
222 Err(StatusCode::BAD_REQUEST)
223 }
224 }
225}
226
227pub async fn revoke_access(
231 State(state): State<AccessReviewState>,
232 Path(review_id): Path<String>,
233 claims: OptionalAuthClaims,
234 Json(request): Json<RevokeAccessRequest>,
235) -> Result<Json<ReviewActionResponse>, StatusCode> {
236 let mut service = state.service.write().await;
237
238 let revoker_id = extract_user_id_with_fallback(&claims);
240
241 match service
242 .revoke_user_access(&review_id, request.user_id, revoker_id, request.reason.clone())
243 .await
244 {
245 Ok(()) => {
246 info!("Access revoked for user {} in review {}", request.user_id, review_id);
247
248 let event = SecurityEvent::new(SecurityEventType::AccessUserSuspended, None, None)
250 .with_actor(EventActor {
251 user_id: Some(revoker_id.to_string()),
252 username: None,
253 ip_address: None,
254 user_agent: None,
255 })
256 .with_target(EventTarget {
257 resource_type: Some("access_review".to_string()),
258 resource_id: Some(review_id.clone()),
259 method: None,
260 })
261 .with_outcome(EventOutcome {
262 success: true,
263 reason: Some(request.reason.clone()),
264 })
265 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
266 .with_metadata("review_id".to_string(), serde_json::json!(review_id));
267 emit_security_event(event).await;
268
269 Ok(Json(ReviewActionResponse {
270 review_id,
271 user_id: request.user_id,
272 status: "revoked".to_string(),
273 timestamp: chrono::Utc::now(),
274 message: Some(format!("Access revoked: {}", request.reason)),
275 }))
276 }
277 Err(e) => {
278 error!("Failed to revoke access: {}", e);
279 Err(StatusCode::BAD_REQUEST)
280 }
281 }
282}
283
284pub async fn update_permissions(
288 State(state): State<AccessReviewState>,
289 Path(review_id): Path<String>,
290 claims: OptionalAuthClaims,
291 Json(request): Json<UpdatePermissionsRequest>,
292) -> Result<Json<ReviewActionResponse>, StatusCode> {
293 let mut service = state.service.write().await;
294
295 let updater_id = extract_user_id_with_fallback(&claims);
297
298 match service
299 .update_user_permissions(
300 &review_id,
301 request.user_id,
302 updater_id,
303 request.roles.clone(),
304 request.permissions.clone(),
305 request.reason.clone(),
306 )
307 .await
308 {
309 Ok(()) => {
310 info!("Permissions updated for user {} in review {}", request.user_id, review_id);
311
312 let event = SecurityEvent::new(SecurityEventType::AuthzPermissionChanged, None, None)
314 .with_actor(EventActor {
315 user_id: Some(updater_id.to_string()),
316 username: None,
317 ip_address: None,
318 user_agent: None,
319 })
320 .with_target(EventTarget {
321 resource_type: Some("access_review".to_string()),
322 resource_id: Some(review_id.clone()),
323 method: None,
324 })
325 .with_outcome(EventOutcome {
326 success: true,
327 reason: request.reason.clone(),
328 })
329 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
330 .with_metadata("review_id".to_string(), serde_json::json!(review_id))
331 .with_metadata("new_roles".to_string(), serde_json::json!(request.roles))
332 .with_metadata(
333 "new_permissions".to_string(),
334 serde_json::json!(request.permissions),
335 );
336 emit_security_event(event).await;
337
338 Ok(Json(ReviewActionResponse {
339 review_id,
340 user_id: request.user_id,
341 status: "permissions_updated".to_string(),
342 timestamp: chrono::Utc::now(),
343 message: Some("Permissions updated successfully".to_string()),
344 }))
345 }
346 Err(e) => {
347 error!("Failed to update permissions: {}", e);
348 Err(StatusCode::BAD_REQUEST)
349 }
350 }
351}
352
353pub async fn get_review_report(
357 State(state): State<AccessReviewState>,
358 Path(review_id): Path<String>,
359) -> Result<Json<serde_json::Value>, StatusCode> {
360 let service = state.service.read().await;
361
362 let review = service.get_review(&review_id).ok_or_else(|| {
363 error!("Review {} not found", review_id);
364 StatusCode::NOT_FOUND
365 })?;
366
367 let report = serde_json::json!({
369 "review_id": review.review_id,
370 "review_date": review.review_date,
371 "review_type": format!("{:?}", review.review_type),
372 "status": format!("{:?}", review.status),
373 "total_items": review.total_items,
374 "items_reviewed": review.items_reviewed,
375 "findings": review.findings,
376 "actions_taken": review.actions_taken,
377 "pending_reviews": review.pending_approvals,
378 "next_review_date": review.next_review_date,
379 });
380
381 Ok(Json(report))
382}
383
384pub async fn start_review(
388 State(state): State<AccessReviewState>,
389 Json(request): Json<StartReviewRequest>,
390) -> Result<Json<ReviewDetailResponse>, StatusCode> {
391 let mut service = state.service.write().await;
392
393 let review_id = match request.review_type {
395 ReviewType::UserAccess => service.start_user_access_review().await.map_err(|e| {
396 error!("Failed to start user access review: {}", e);
397 StatusCode::INTERNAL_SERVER_ERROR
398 })?,
399 ReviewType::PrivilegedAccess => {
400 service.start_privileged_access_review().await.map_err(|e| {
401 error!("Failed to start privileged access review: {}", e);
402 StatusCode::INTERNAL_SERVER_ERROR
403 })?
404 }
405 ReviewType::ApiToken => service.start_token_review().await.map_err(|e| {
406 error!("Failed to start token review: {}", e);
407 StatusCode::INTERNAL_SERVER_ERROR
408 })?,
409 ReviewType::ResourceAccess => {
410 service.start_resource_access_review(Vec::new()).await.map_err(|e| {
411 error!("Failed to start resource access review: {}", e);
412 StatusCode::INTERNAL_SERVER_ERROR
413 })?
414 }
415 };
416
417 info!("Started access review: {}", review_id);
418
419 let review = service
421 .get_review(&review_id)
422 .ok_or_else(|| {
423 error!("Review {} not found after creation", review_id);
424 StatusCode::INTERNAL_SERVER_ERROR
425 })?
426 .clone();
427
428 let event = SecurityEvent::new(SecurityEventType::ComplianceComplianceCheck, None, None)
430 .with_target(EventTarget {
431 resource_type: Some("access_review".to_string()),
432 resource_id: Some(review_id.clone()),
433 method: None,
434 })
435 .with_outcome(EventOutcome {
436 success: true,
437 reason: Some("Access review started".to_string()),
438 });
439 emit_security_event(event).await;
440
441 let items = service
442 .engine()
443 .get_review_items(&review_id)
444 .map(|items_map| items_map.values().cloned().collect());
445
446 Ok(Json(ReviewDetailResponse { review, items }))
447}
448
449#[derive(Debug, Deserialize)]
451pub struct StartReviewRequest {
452 pub review_type: ReviewType,
454}
455
456pub fn access_review_router(state: AccessReviewState) -> axum::Router {
458 use axum::routing::{get, post};
459
460 axum::Router::new()
461 .route("/", get(list_reviews))
462 .route("/start", post(start_review))
463 .route("/{review_id}", get(get_review))
464 .route("/{review_id}/approve", post(approve_access))
465 .route("/{review_id}/revoke", post(revoke_access))
466 .route("/{review_id}/update-permissions", post(update_permissions))
467 .route("/{review_id}/report", get(get_review_report))
468 .with_state(state)
469}