1use axum::{
10 extract::{Path, Query, 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::collections::HashMap;
21use std::sync::Arc;
22use tokio::sync::RwLock;
23use tracing::{debug, error, info};
24use uuid::Uuid;
25
26use crate::handlers::auth_helpers::{extract_user_id_with_fallback, OptionalAuthClaims};
27
28#[derive(Clone)]
30pub struct AccessReviewState {
31 pub service: Arc<RwLock<AccessReviewService>>,
33}
34
35#[derive(Debug, Deserialize)]
37pub struct ApproveAccessRequest {
38 pub user_id: Uuid,
40 pub approved: bool,
42 pub justification: Option<String>,
44}
45
46#[derive(Debug, Deserialize)]
48pub struct RevokeAccessRequest {
49 pub user_id: Uuid,
51 pub reason: String,
53}
54
55#[derive(Debug, Serialize)]
57pub struct ReviewListResponse {
58 pub reviews: Vec<ReviewSummary>,
60}
61
62#[derive(Debug, Serialize)]
64pub struct ReviewSummary {
65 pub review_id: String,
67 pub review_type: String,
69 pub status: String,
71 pub due_date: chrono::DateTime<chrono::Utc>,
73 pub items_count: u32,
75 pub pending_approvals: u32,
77}
78
79#[derive(Debug, Serialize)]
81pub struct ReviewDetailResponse {
82 #[serde(flatten)]
84 pub review: AccessReview,
85 pub items: Option<Vec<UserReviewItem>>,
87}
88
89#[derive(Debug, Serialize)]
91pub struct ReviewActionResponse {
92 pub review_id: String,
94 pub user_id: Uuid,
96 pub status: String,
98 pub timestamp: chrono::DateTime<chrono::Utc>,
100 pub message: Option<String>,
102}
103
104pub async fn list_reviews(
108 State(state): State<AccessReviewState>,
109) -> Result<Json<ReviewListResponse>, StatusCode> {
110 let service = state.service.read().await;
111 let reviews = service.get_all_reviews();
112
113 let summaries: Vec<ReviewSummary> = reviews
114 .iter()
115 .map(|review| ReviewSummary {
116 review_id: review.review_id.clone(),
117 review_type: format!("{:?}", review.review_type),
118 status: format!("{:?}", review.status),
119 due_date: review.due_date,
120 items_count: review.total_items,
121 pending_approvals: review.pending_approvals,
122 })
123 .collect();
124
125 Ok(Json(ReviewListResponse { reviews: summaries }))
126}
127
128pub async fn get_review(
132 State(state): State<AccessReviewState>,
133 Path(review_id): Path<String>,
134) -> Result<Json<ReviewDetailResponse>, StatusCode> {
135 let service = state.service.read().await;
136
137 let review = service
138 .get_review(&review_id)
139 .ok_or_else(|| {
140 error!("Review {} not found", review_id);
141 StatusCode::NOT_FOUND
142 })?
143 .clone();
144
145 let items = service
147 .engine()
148 .get_review_items(&review_id)
149 .map(|items_map| items_map.values().cloned().collect());
150
151 Ok(Json(ReviewDetailResponse { review, items }))
152}
153
154pub async fn approve_access(
158 State(state): State<AccessReviewState>,
159 Path(review_id): Path<String>,
160 claims: OptionalAuthClaims,
161 Json(request): Json<ApproveAccessRequest>,
162) -> Result<Json<ReviewActionResponse>, StatusCode> {
163 let mut service = state.service.write().await;
164
165 let approver_id = extract_user_id_with_fallback(&claims);
167
168 match service
169 .approve_user_access(&review_id, request.user_id, approver_id, request.justification)
170 .await
171 {
172 Ok(()) => {
173 info!("Access approved for user {} in review {}", request.user_id, review_id);
174
175 let event = SecurityEvent::new(SecurityEventType::AuthzAccessGranted, None, None)
177 .with_actor(EventActor {
178 user_id: Some(approver_id.to_string()),
179 username: None,
180 ip_address: None,
181 user_agent: None,
182 })
183 .with_target(EventTarget {
184 resource_type: Some("access_review".to_string()),
185 resource_id: Some(review_id.clone()),
186 method: None,
187 })
188 .with_outcome(EventOutcome {
189 success: true,
190 reason: Some("Access approved in review".to_string()),
191 })
192 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id));
193 emit_security_event(event).await;
194
195 Ok(Json(ReviewActionResponse {
196 review_id,
197 user_id: request.user_id,
198 status: "approved".to_string(),
199 timestamp: chrono::Utc::now(),
200 message: Some("Access approved successfully".to_string()),
201 }))
202 }
203 Err(e) => {
204 error!("Failed to approve access: {}", e);
205 Err(StatusCode::BAD_REQUEST)
206 }
207 }
208}
209
210pub async fn revoke_access(
214 State(state): State<AccessReviewState>,
215 Path(review_id): Path<String>,
216 claims: OptionalAuthClaims,
217 Json(request): Json<RevokeAccessRequest>,
218) -> Result<Json<ReviewActionResponse>, StatusCode> {
219 let mut service = state.service.write().await;
220
221 let revoker_id = extract_user_id_with_fallback(&claims);
223
224 match service
225 .revoke_user_access(&review_id, request.user_id, revoker_id, request.reason.clone())
226 .await
227 {
228 Ok(()) => {
229 info!("Access revoked for user {} in review {}", request.user_id, review_id);
230
231 let event = SecurityEvent::new(SecurityEventType::AccessUserSuspended, None, None)
233 .with_actor(EventActor {
234 user_id: Some(revoker_id.to_string()),
235 username: None,
236 ip_address: None,
237 user_agent: None,
238 })
239 .with_target(EventTarget {
240 resource_type: Some("access_review".to_string()),
241 resource_id: Some(review_id.clone()),
242 method: None,
243 })
244 .with_outcome(EventOutcome {
245 success: true,
246 reason: Some(request.reason.clone()),
247 })
248 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
249 .with_metadata("review_id".to_string(), serde_json::json!(review_id));
250 emit_security_event(event).await;
251
252 Ok(Json(ReviewActionResponse {
253 review_id,
254 user_id: request.user_id,
255 status: "revoked".to_string(),
256 timestamp: chrono::Utc::now(),
257 message: Some(format!("Access revoked: {}", request.reason)),
258 }))
259 }
260 Err(e) => {
261 error!("Failed to revoke access: {}", e);
262 Err(StatusCode::BAD_REQUEST)
263 }
264 }
265}
266
267pub async fn get_review_report(
271 State(state): State<AccessReviewState>,
272 Path(review_id): Path<String>,
273) -> Result<Json<serde_json::Value>, StatusCode> {
274 let service = state.service.read().await;
275
276 let review = service.get_review(&review_id).ok_or_else(|| {
277 error!("Review {} not found", review_id);
278 StatusCode::NOT_FOUND
279 })?;
280
281 let report = serde_json::json!({
283 "review_id": review.review_id,
284 "review_date": review.review_date,
285 "review_type": format!("{:?}", review.review_type),
286 "status": format!("{:?}", review.status),
287 "total_items": review.total_items,
288 "items_reviewed": review.items_reviewed,
289 "findings": review.findings,
290 "actions_taken": review.actions_taken,
291 "pending_reviews": review.pending_approvals,
292 "next_review_date": review.next_review_date,
293 });
294
295 Ok(Json(report))
296}
297
298pub async fn start_review(
302 State(state): State<AccessReviewState>,
303 Json(request): Json<StartReviewRequest>,
304) -> Result<Json<ReviewDetailResponse>, StatusCode> {
305 let mut service = state.service.write().await;
306
307 let review_id = match request.review_type {
309 ReviewType::UserAccess => service.start_user_access_review().await.map_err(|e| {
310 error!("Failed to start user access review: {}", e);
311 StatusCode::INTERNAL_SERVER_ERROR
312 })?,
313 ReviewType::PrivilegedAccess => {
314 service.start_privileged_access_review().await.map_err(|e| {
315 error!("Failed to start privileged access review: {}", e);
316 StatusCode::INTERNAL_SERVER_ERROR
317 })?
318 }
319 ReviewType::ApiToken => service.start_token_review().await.map_err(|e| {
320 error!("Failed to start token review: {}", e);
321 StatusCode::INTERNAL_SERVER_ERROR
322 })?,
323 ReviewType::ResourceAccess => {
324 return Err(StatusCode::NOT_IMPLEMENTED);
325 }
326 };
327
328 info!("Started access review: {}", review_id);
329
330 let review = service
332 .get_review(&review_id)
333 .ok_or_else(|| {
334 error!("Review {} not found after creation", review_id);
335 StatusCode::INTERNAL_SERVER_ERROR
336 })?
337 .clone();
338
339 let event = SecurityEvent::new(SecurityEventType::ComplianceComplianceCheck, None, None)
341 .with_target(EventTarget {
342 resource_type: Some("access_review".to_string()),
343 resource_id: Some(review_id.clone()),
344 method: None,
345 })
346 .with_outcome(EventOutcome {
347 success: true,
348 reason: Some("Access review started".to_string()),
349 });
350 emit_security_event(event).await;
351
352 let items = service
353 .engine()
354 .get_review_items(&review_id)
355 .map(|items_map| items_map.values().cloned().collect());
356
357 Ok(Json(ReviewDetailResponse { review, items }))
358}
359
360#[derive(Debug, Deserialize)]
362pub struct StartReviewRequest {
363 pub review_type: ReviewType,
365}
366
367pub fn access_review_router(state: AccessReviewState) -> axum::Router {
369 use axum::routing::{get, post};
370
371 axum::Router::new()
372 .route("/", get(list_reviews))
373 .route("/start", post(start_review))
374 .route("/{review_id}", get(get_review))
375 .route("/{review_id}/approve", post(approve_access))
376 .route("/{review_id}/revoke", post(revoke_access))
377 .route("/{review_id}/report", get(get_review_report))
378 .with_state(state)
379}