mockforge_http/handlers/
access_review.rs

1//! Access review API handlers
2//!
3//! Provides HTTP endpoints for managing access reviews, including:
4//! - Listing reviews
5//! - Approving/revoking access
6//! - Getting review reports
7//! - Starting reviews
8
9use 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/// State for access review handlers
32#[derive(Clone)]
33pub struct AccessReviewState {
34    /// Access review service
35    pub service: Arc<RwLock<AccessReviewService>>,
36}
37
38/// Request to approve access in a review
39#[derive(Debug, Deserialize)]
40pub struct ApproveAccessRequest {
41    /// User ID to approve
42    pub user_id: Uuid,
43    /// Whether access is approved
44    pub approved: bool,
45    /// Justification for approval
46    pub justification: Option<String>,
47}
48
49/// Request to revoke access in a review
50#[derive(Debug, Deserialize)]
51pub struct RevokeAccessRequest {
52    /// User ID to revoke
53    pub user_id: Uuid,
54    /// Reason for revocation
55    pub reason: String,
56}
57
58/// Response for review list
59#[derive(Debug, Serialize)]
60pub struct ReviewListResponse {
61    /// List of reviews
62    pub reviews: Vec<ReviewSummary>,
63}
64
65/// Review summary (simplified review for list view)
66#[derive(Debug, Serialize)]
67pub struct ReviewSummary {
68    /// Review ID
69    pub review_id: String,
70    /// Review type
71    pub review_type: String,
72    /// Review status
73    pub status: String,
74    /// Due date
75    pub due_date: chrono::DateTime<chrono::Utc>,
76    /// Total items count
77    pub items_count: u32,
78    /// Pending approvals count
79    pub pending_approvals: u32,
80}
81
82/// Response for review details
83#[derive(Debug, Serialize)]
84pub struct ReviewDetailResponse {
85    /// Review details
86    #[serde(flatten)]
87    pub review: AccessReview,
88    /// Review items (if user access review)
89    pub items: Option<Vec<UserReviewItem>>,
90}
91
92/// Response for approve/revoke operations
93#[derive(Debug, Serialize)]
94pub struct ReviewActionResponse {
95    /// Review ID
96    pub review_id: String,
97    /// User ID
98    pub user_id: Uuid,
99    /// Action status
100    pub status: String,
101    /// Action timestamp
102    pub timestamp: chrono::DateTime<chrono::Utc>,
103    /// Additional message
104    pub message: Option<String>,
105}
106
107/// Get all access reviews
108///
109/// GET /api/v1/security/access-reviews
110pub 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
131/// Get a specific access review
132///
133/// GET /api/v1/security/access-reviews/{review_id}
134pub 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    // Get review items if available
149    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
157/// Approve access in a review
158///
159/// POST /api/v1/security/access-reviews/{review_id}/approve
160pub 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    // Extract approver ID from authentication claims, or use default for mock server
169    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            // Emit security event
179            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
213/// Revoke access in a review
214///
215/// POST /api/v1/security/access-reviews/{review_id}/revoke
216pub 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    // Extract revoker ID from authentication claims, or use default for mock server
225    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            // Emit security event
235            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
270/// Get review report
271///
272/// GET /api/v1/security/access-reviews/{review_id}/report
273pub 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    // Convert review to JSON report format
287    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
303/// Start a new access review
304///
305/// POST /api/v1/security/access-reviews/start
306pub 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    // Start review based on type
313    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    // Get the review details
343    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    // Emit security event
351    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/// Request to start a review
375#[derive(Debug, Deserialize)]
376pub struct StartReviewRequest {
377    /// Review type to start
378    pub review_type: ReviewType,
379}
380
381/// Create access review router
382pub 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}