1use axum::{
10 extract::{Extension, Path, Query, State},
11 http::StatusCode,
12 response::Json,
13};
14use mockforge_core::security::{
15 access_review::{
16 AccessReview, ReviewType, UserReviewItem,
17 },
18 access_review_service::AccessReviewService,
19 emit_security_event, EventActor, EventOutcome, EventTarget, SecurityEvent, SecurityEventType,
20};
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::sync::Arc;
24use tokio::sync::RwLock;
25use tracing::{debug, error, info};
26use uuid::Uuid;
27
28use crate::auth::types::AuthClaims;
29use crate::handlers::auth_helpers::extract_user_id_with_fallback;
30
31#[derive(Clone)]
33pub struct AccessReviewState {
34 pub service: Arc<RwLock<AccessReviewService>>,
36}
37
38#[derive(Debug, Deserialize)]
40pub struct ApproveAccessRequest {
41 pub user_id: Uuid,
43 pub approved: bool,
45 pub justification: Option<String>,
47}
48
49#[derive(Debug, Deserialize)]
51pub struct RevokeAccessRequest {
52 pub user_id: Uuid,
54 pub reason: String,
56}
57
58#[derive(Debug, Serialize)]
60pub struct ReviewListResponse {
61 pub reviews: Vec<ReviewSummary>,
63}
64
65#[derive(Debug, Serialize)]
67pub struct ReviewSummary {
68 pub review_id: String,
70 pub review_type: String,
72 pub status: String,
74 pub due_date: chrono::DateTime<chrono::Utc>,
76 pub items_count: u32,
78 pub pending_approvals: u32,
80}
81
82#[derive(Debug, Serialize)]
84pub struct ReviewDetailResponse {
85 #[serde(flatten)]
87 pub review: AccessReview,
88 pub items: Option<Vec<UserReviewItem>>,
90}
91
92#[derive(Debug, Serialize)]
94pub struct ReviewActionResponse {
95 pub review_id: String,
97 pub user_id: Uuid,
99 pub status: String,
101 pub timestamp: chrono::DateTime<chrono::Utc>,
103 pub message: Option<String>,
105}
106
107pub async fn list_reviews(
111 State(state): State<AccessReviewState>,
112) -> Result<Json<ReviewListResponse>, StatusCode> {
113 let service = state.service.read().await;
114 let reviews = service.get_all_reviews();
115
116 let summaries: Vec<ReviewSummary> = reviews
117 .iter()
118 .map(|review| ReviewSummary {
119 review_id: review.review_id.clone(),
120 review_type: format!("{:?}", review.review_type),
121 status: format!("{:?}", review.status),
122 due_date: review.due_date,
123 items_count: review.total_items,
124 pending_approvals: review.pending_approvals,
125 })
126 .collect();
127
128 Ok(Json(ReviewListResponse { reviews: summaries }))
129}
130
131pub async fn get_review(
135 State(state): State<AccessReviewState>,
136 Path(review_id): Path<String>,
137) -> Result<Json<ReviewDetailResponse>, StatusCode> {
138 let service = state.service.read().await;
139
140 let review = service
141 .get_review(&review_id)
142 .ok_or_else(|| {
143 error!("Review {} not found", review_id);
144 StatusCode::NOT_FOUND
145 })?
146 .clone();
147
148 let items = service
150 .engine()
151 .get_review_items(&review_id)
152 .map(|items_map| items_map.values().cloned().collect());
153
154 Ok(Json(ReviewDetailResponse { review, items }))
155}
156
157pub async fn approve_access(
161 State(state): State<AccessReviewState>,
162 Path(review_id): Path<String>,
163 Json(request): Json<ApproveAccessRequest>,
164 claims: Option<Extension<AuthClaims>>,
165) -> Result<Json<ReviewActionResponse>, StatusCode> {
166 let mut service = state.service.write().await;
167
168 let approver_id = extract_user_id_with_fallback(claims);
170
171 match service.approve_user_access(&review_id, request.user_id, approver_id, request.justification).await {
172 Ok(()) => {
173 info!(
174 "Access approved for user {} in review {}",
175 request.user_id, review_id
176 );
177
178 let event = SecurityEvent::new(SecurityEventType::AuthzAccessGranted, None, None)
180 .with_actor(EventActor {
181 user_id: Some(approver_id.to_string()),
182 username: None,
183 ip_address: None,
184 user_agent: None,
185 })
186 .with_target(EventTarget {
187 resource_type: Some("access_review".to_string()),
188 resource_id: Some(review_id.clone()),
189 method: None,
190 })
191 .with_outcome(EventOutcome {
192 success: true,
193 reason: Some("Access approved in review".to_string()),
194 })
195 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id));
196 emit_security_event(event).await;
197
198 Ok(Json(ReviewActionResponse {
199 review_id,
200 user_id: request.user_id,
201 status: "approved".to_string(),
202 timestamp: chrono::Utc::now(),
203 message: Some("Access approved successfully".to_string()),
204 }))
205 }
206 Err(e) => {
207 error!("Failed to approve access: {}", e);
208 Err(StatusCode::BAD_REQUEST)
209 }
210 }
211}
212
213pub async fn revoke_access(
217 State(state): State<AccessReviewState>,
218 Path(review_id): Path<String>,
219 Json(request): Json<RevokeAccessRequest>,
220 claims: Option<Extension<AuthClaims>>,
221) -> Result<Json<ReviewActionResponse>, StatusCode> {
222 let mut service = state.service.write().await;
223
224 let revoker_id = extract_user_id_with_fallback(claims);
226
227 match service.revoke_user_access(&review_id, request.user_id, revoker_id, request.reason.clone()).await {
228 Ok(()) => {
229 info!(
230 "Access revoked for user {} in review {}",
231 request.user_id, review_id
232 );
233
234 let event = SecurityEvent::new(SecurityEventType::AccessUserSuspended, None, None)
236 .with_actor(EventActor {
237 user_id: Some(revoker_id.to_string()),
238 username: None,
239 ip_address: None,
240 user_agent: None,
241 })
242 .with_target(EventTarget {
243 resource_type: Some("access_review".to_string()),
244 resource_id: Some(review_id.clone()),
245 method: None,
246 })
247 .with_outcome(EventOutcome {
248 success: true,
249 reason: Some(request.reason.clone()),
250 })
251 .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
252 .with_metadata("review_id".to_string(), serde_json::json!(review_id));
253 emit_security_event(event).await;
254
255 Ok(Json(ReviewActionResponse {
256 review_id,
257 user_id: request.user_id,
258 status: "revoked".to_string(),
259 timestamp: chrono::Utc::now(),
260 message: Some(format!("Access revoked: {}", request.reason)),
261 }))
262 }
263 Err(e) => {
264 error!("Failed to revoke access: {}", e);
265 Err(StatusCode::BAD_REQUEST)
266 }
267 }
268}
269
270pub async fn get_review_report(
274 State(state): State<AccessReviewState>,
275 Path(review_id): Path<String>,
276) -> Result<Json<serde_json::Value>, StatusCode> {
277 let service = state.service.read().await;
278
279 let review = service
280 .get_review(&review_id)
281 .ok_or_else(|| {
282 error!("Review {} not found", review_id);
283 StatusCode::NOT_FOUND
284 })?;
285
286 let report = serde_json::json!({
288 "review_id": review.review_id,
289 "review_date": review.review_date,
290 "review_type": format!("{:?}", review.review_type),
291 "status": format!("{:?}", review.status),
292 "total_items": review.total_items,
293 "items_reviewed": review.items_reviewed,
294 "findings": review.findings,
295 "actions_taken": review.actions_taken,
296 "pending_reviews": review.pending_approvals,
297 "next_review_date": review.next_review_date,
298 });
299
300 Ok(Json(report))
301}
302
303pub async fn start_review(
307 State(state): State<AccessReviewState>,
308 Json(request): Json<StartReviewRequest>,
309) -> Result<Json<ReviewDetailResponse>, StatusCode> {
310 let mut service = state.service.write().await;
311
312 let review_id = match request.review_type {
314 ReviewType::UserAccess => {
315 service.start_user_access_review().await
316 .map_err(|e| {
317 error!("Failed to start user access review: {}", e);
318 StatusCode::INTERNAL_SERVER_ERROR
319 })?
320 }
321 ReviewType::PrivilegedAccess => {
322 service.start_privileged_access_review().await
323 .map_err(|e| {
324 error!("Failed to start privileged access review: {}", e);
325 StatusCode::INTERNAL_SERVER_ERROR
326 })?
327 }
328 ReviewType::ApiToken => {
329 service.start_token_review().await
330 .map_err(|e| {
331 error!("Failed to start token review: {}", e);
332 StatusCode::INTERNAL_SERVER_ERROR
333 })?
334 }
335 ReviewType::ResourceAccess => {
336 return Err(StatusCode::NOT_IMPLEMENTED);
337 }
338 };
339
340 info!("Started access review: {}", review_id);
341
342 let review = service.get_review(&review_id)
344 .ok_or_else(|| {
345 error!("Review {} not found after creation", review_id);
346 StatusCode::INTERNAL_SERVER_ERROR
347 })?
348 .clone();
349
350 let event = SecurityEvent::new(SecurityEventType::ComplianceComplianceCheck, None, None)
352 .with_target(EventTarget {
353 resource_type: Some("access_review".to_string()),
354 resource_id: Some(review_id.clone()),
355 method: None,
356 })
357 .with_outcome(EventOutcome {
358 success: true,
359 reason: Some("Access review started".to_string()),
360 });
361 emit_security_event(event).await;
362
363 let items = service
364 .engine()
365 .get_review_items(&review_id)
366 .map(|items_map| items_map.values().cloned().collect());
367
368 Ok(Json(ReviewDetailResponse {
369 review,
370 items,
371 }))
372}
373
374#[derive(Debug, Deserialize)]
376pub struct StartReviewRequest {
377 pub review_type: ReviewType,
379}
380
381pub fn access_review_router(state: AccessReviewState) -> axum::Router {
383 use axum::routing::{get, post};
384
385 axum::Router::new()
386 .route("/", get(list_reviews))
387 .route("/start", post(start_review))
388 .route("/{review_id}", get(get_review))
389 .route("/{review_id}/approve", post(approve_access))
390 .route("/{review_id}/revoke", post(revoke_access))
391 .route("/{review_id}/report", get(get_review_report))
392 .with_state(state)
393}