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::{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/// State for access review handlers
29#[derive(Clone)]
30pub struct AccessReviewState {
31    /// Access review service
32    pub service: Arc<RwLock<AccessReviewService>>,
33}
34
35/// Request to approve access in a review
36#[derive(Debug, Deserialize)]
37pub struct ApproveAccessRequest {
38    /// User ID to approve
39    pub user_id: Uuid,
40    /// Whether access is approved
41    pub approved: bool,
42    /// Justification for approval
43    pub justification: Option<String>,
44}
45
46/// Request to revoke access in a review
47#[derive(Debug, Deserialize)]
48pub struct RevokeAccessRequest {
49    /// User ID to revoke
50    pub user_id: Uuid,
51    /// Reason for revocation
52    pub reason: String,
53}
54
55/// Response for review list
56#[derive(Debug, Serialize)]
57pub struct ReviewListResponse {
58    /// List of reviews
59    pub reviews: Vec<ReviewSummary>,
60}
61
62/// Review summary (simplified review for list view)
63#[derive(Debug, Serialize)]
64pub struct ReviewSummary {
65    /// Review ID
66    pub review_id: String,
67    /// Review type
68    pub review_type: String,
69    /// Review status
70    pub status: String,
71    /// Due date
72    pub due_date: chrono::DateTime<chrono::Utc>,
73    /// Total items count
74    pub items_count: u32,
75    /// Pending approvals count
76    pub pending_approvals: u32,
77}
78
79/// Response for review details
80#[derive(Debug, Serialize)]
81pub struct ReviewDetailResponse {
82    /// Review details
83    #[serde(flatten)]
84    pub review: AccessReview,
85    /// Review items (if user access review)
86    pub items: Option<Vec<UserReviewItem>>,
87}
88
89/// Response for approve/revoke operations
90#[derive(Debug, Serialize)]
91pub struct ReviewActionResponse {
92    /// Review ID
93    pub review_id: String,
94    /// User ID
95    pub user_id: Uuid,
96    /// Action status
97    pub status: String,
98    /// Action timestamp
99    pub timestamp: chrono::DateTime<chrono::Utc>,
100    /// Additional message
101    pub message: Option<String>,
102}
103
104/// Get all access reviews
105///
106/// GET /api/v1/security/access-reviews
107pub 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
128/// Get a specific access review
129///
130/// GET /api/v1/security/access-reviews/{review_id}
131pub 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    // Get review items if available
146    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
154/// Approve access in a review
155///
156/// POST /api/v1/security/access-reviews/{review_id}/approve
157pub 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    // Extract approver ID from authentication claims, or use default for mock server
166    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            // Emit security event
176            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
210/// Revoke access in a review
211///
212/// POST /api/v1/security/access-reviews/{review_id}/revoke
213pub 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    // Extract revoker ID from authentication claims, or use default for mock server
222    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            // Emit security event
232            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
267/// Get review report
268///
269/// GET /api/v1/security/access-reviews/{review_id}/report
270pub 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    // Convert review to JSON report format
282    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
298/// Start a new access review
299///
300/// POST /api/v1/security/access-reviews/start
301pub 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    // Start review based on type
308    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    // Get the review details
331    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    // Emit security event
340    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/// Request to start a review
361#[derive(Debug, Deserialize)]
362pub struct StartReviewRequest {
363    /// Review type to start
364    pub review_type: ReviewType,
365}
366
367/// Create access review router
368pub 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}