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, 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::sync::Arc;
21use tokio::sync::RwLock;
22use tracing::{error, info};
23use uuid::Uuid;
24
25use crate::handlers::auth_helpers::{extract_user_id_with_fallback, OptionalAuthClaims};
26
27/// State for access review handlers
28#[derive(Clone)]
29pub struct AccessReviewState {
30    /// Access review service
31    pub service: Arc<RwLock<AccessReviewService>>,
32}
33
34/// Request to approve access in a review
35#[derive(Debug, Deserialize)]
36pub struct ApproveAccessRequest {
37    /// User ID to approve
38    pub user_id: Uuid,
39    /// Whether access is approved
40    pub approved: bool,
41    /// Justification for approval
42    pub justification: Option<String>,
43}
44
45/// Request to revoke access in a review
46#[derive(Debug, Deserialize)]
47pub struct RevokeAccessRequest {
48    /// User ID to revoke
49    pub user_id: Uuid,
50    /// Reason for revocation
51    pub reason: String,
52}
53
54/// Request to update permissions in a review
55#[derive(Debug, Deserialize)]
56pub struct UpdatePermissionsRequest {
57    /// User ID to update
58    pub user_id: Uuid,
59    /// New roles for the user
60    pub roles: Vec<String>,
61    /// New permissions for the user
62    pub permissions: Vec<String>,
63    /// Reason for permission update
64    pub reason: Option<String>,
65}
66
67/// Response for review list
68#[derive(Debug, Serialize)]
69pub struct ReviewListResponse {
70    /// List of reviews
71    pub reviews: Vec<ReviewSummary>,
72}
73
74/// Review summary (simplified review for list view)
75#[derive(Debug, Serialize)]
76pub struct ReviewSummary {
77    /// Review ID
78    pub review_id: String,
79    /// Review type
80    pub review_type: String,
81    /// Review status
82    pub status: String,
83    /// Due date
84    pub due_date: chrono::DateTime<chrono::Utc>,
85    /// Total items count
86    pub items_count: u32,
87    /// Pending approvals count
88    pub pending_approvals: u32,
89}
90
91/// Response for review details
92#[derive(Debug, Serialize)]
93pub struct ReviewDetailResponse {
94    /// Review details
95    #[serde(flatten)]
96    pub review: AccessReview,
97    /// Review items (if user access review)
98    pub items: Option<Vec<UserReviewItem>>,
99}
100
101/// Response for approve/revoke operations
102#[derive(Debug, Serialize)]
103pub struct ReviewActionResponse {
104    /// Review ID
105    pub review_id: String,
106    /// User ID
107    pub user_id: Uuid,
108    /// Action status
109    pub status: String,
110    /// Action timestamp
111    pub timestamp: chrono::DateTime<chrono::Utc>,
112    /// Additional message
113    pub message: Option<String>,
114}
115
116/// Get all access reviews
117///
118/// GET /api/v1/security/access-reviews
119pub async fn list_reviews(
120    State(state): State<AccessReviewState>,
121) -> Result<Json<ReviewListResponse>, StatusCode> {
122    let service = state.service.read().await;
123    let reviews = service.get_all_reviews();
124
125    let summaries: Vec<ReviewSummary> = reviews
126        .iter()
127        .map(|review| ReviewSummary {
128            review_id: review.review_id.clone(),
129            review_type: format!("{:?}", review.review_type),
130            status: format!("{:?}", review.status),
131            due_date: review.due_date,
132            items_count: review.total_items,
133            pending_approvals: review.pending_approvals,
134        })
135        .collect();
136
137    Ok(Json(ReviewListResponse { reviews: summaries }))
138}
139
140/// Get a specific access review
141///
142/// GET /api/v1/security/access-reviews/{review_id}
143pub async fn get_review(
144    State(state): State<AccessReviewState>,
145    Path(review_id): Path<String>,
146) -> Result<Json<ReviewDetailResponse>, StatusCode> {
147    let service = state.service.read().await;
148
149    let review = service
150        .get_review(&review_id)
151        .ok_or_else(|| {
152            error!("Review {} not found", review_id);
153            StatusCode::NOT_FOUND
154        })?
155        .clone();
156
157    // Get review items if available
158    let items = service
159        .engine()
160        .get_review_items(&review_id)
161        .map(|items_map| items_map.values().cloned().collect());
162
163    Ok(Json(ReviewDetailResponse { review, items }))
164}
165
166/// Approve access in a review
167///
168/// POST /api/v1/security/access-reviews/{review_id}/approve
169pub async fn approve_access(
170    State(state): State<AccessReviewState>,
171    Path(review_id): Path<String>,
172    claims: OptionalAuthClaims,
173    Json(request): Json<ApproveAccessRequest>,
174) -> Result<Json<ReviewActionResponse>, StatusCode> {
175    let mut service = state.service.write().await;
176
177    // Extract approver ID from authentication claims, or use default for mock server
178    let approver_id = extract_user_id_with_fallback(&claims);
179
180    match service
181        .approve_user_access(&review_id, request.user_id, approver_id, request.justification)
182        .await
183    {
184        Ok(()) => {
185            info!("Access approved for user {} in review {}", request.user_id, review_id);
186
187            // Emit security event
188            let event = SecurityEvent::new(SecurityEventType::AuthzAccessGranted, None, None)
189                .with_actor(EventActor {
190                    user_id: Some(approver_id.to_string()),
191                    username: None,
192                    ip_address: None,
193                    user_agent: None,
194                })
195                .with_target(EventTarget {
196                    resource_type: Some("access_review".to_string()),
197                    resource_id: Some(review_id.clone()),
198                    method: None,
199                })
200                .with_outcome(EventOutcome {
201                    success: true,
202                    reason: Some("Access approved in review".to_string()),
203                })
204                .with_metadata("user_id".to_string(), serde_json::json!(request.user_id));
205            emit_security_event(event).await;
206
207            Ok(Json(ReviewActionResponse {
208                review_id,
209                user_id: request.user_id,
210                status: "approved".to_string(),
211                timestamp: chrono::Utc::now(),
212                message: Some("Access approved successfully".to_string()),
213            }))
214        }
215        Err(e) => {
216            error!("Failed to approve access: {}", e);
217            Err(StatusCode::BAD_REQUEST)
218        }
219    }
220}
221
222/// Revoke access in a review
223///
224/// POST /api/v1/security/access-reviews/{review_id}/revoke
225pub async fn revoke_access(
226    State(state): State<AccessReviewState>,
227    Path(review_id): Path<String>,
228    claims: OptionalAuthClaims,
229    Json(request): Json<RevokeAccessRequest>,
230) -> Result<Json<ReviewActionResponse>, StatusCode> {
231    let mut service = state.service.write().await;
232
233    // Extract revoker ID from authentication claims, or use default for mock server
234    let revoker_id = extract_user_id_with_fallback(&claims);
235
236    match service
237        .revoke_user_access(&review_id, request.user_id, revoker_id, request.reason.clone())
238        .await
239    {
240        Ok(()) => {
241            info!("Access revoked for user {} in review {}", request.user_id, review_id);
242
243            // Emit security event
244            let event = SecurityEvent::new(SecurityEventType::AccessUserSuspended, None, None)
245                .with_actor(EventActor {
246                    user_id: Some(revoker_id.to_string()),
247                    username: None,
248                    ip_address: None,
249                    user_agent: None,
250                })
251                .with_target(EventTarget {
252                    resource_type: Some("access_review".to_string()),
253                    resource_id: Some(review_id.clone()),
254                    method: None,
255                })
256                .with_outcome(EventOutcome {
257                    success: true,
258                    reason: Some(request.reason.clone()),
259                })
260                .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
261                .with_metadata("review_id".to_string(), serde_json::json!(review_id));
262            emit_security_event(event).await;
263
264            Ok(Json(ReviewActionResponse {
265                review_id,
266                user_id: request.user_id,
267                status: "revoked".to_string(),
268                timestamp: chrono::Utc::now(),
269                message: Some(format!("Access revoked: {}", request.reason)),
270            }))
271        }
272        Err(e) => {
273            error!("Failed to revoke access: {}", e);
274            Err(StatusCode::BAD_REQUEST)
275        }
276    }
277}
278
279/// Update user permissions in a review
280///
281/// POST /api/v1/security/access-reviews/{review_id}/update-permissions
282pub async fn update_permissions(
283    State(state): State<AccessReviewState>,
284    Path(review_id): Path<String>,
285    claims: OptionalAuthClaims,
286    Json(request): Json<UpdatePermissionsRequest>,
287) -> Result<Json<ReviewActionResponse>, StatusCode> {
288    let mut service = state.service.write().await;
289
290    // Extract updater ID from authentication claims, or use default for mock server
291    let updater_id = extract_user_id_with_fallback(&claims);
292
293    match service
294        .update_user_permissions(
295            &review_id,
296            request.user_id,
297            updater_id,
298            request.roles.clone(),
299            request.permissions.clone(),
300            request.reason.clone(),
301        )
302        .await
303    {
304        Ok(()) => {
305            info!("Permissions updated for user {} in review {}", request.user_id, review_id);
306
307            // Emit security event for permission change
308            let event = SecurityEvent::new(SecurityEventType::AuthzPermissionChanged, None, None)
309                .with_actor(EventActor {
310                    user_id: Some(updater_id.to_string()),
311                    username: None,
312                    ip_address: None,
313                    user_agent: None,
314                })
315                .with_target(EventTarget {
316                    resource_type: Some("access_review".to_string()),
317                    resource_id: Some(review_id.clone()),
318                    method: None,
319                })
320                .with_outcome(EventOutcome {
321                    success: true,
322                    reason: request.reason.clone(),
323                })
324                .with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
325                .with_metadata("review_id".to_string(), serde_json::json!(review_id))
326                .with_metadata("new_roles".to_string(), serde_json::json!(request.roles))
327                .with_metadata(
328                    "new_permissions".to_string(),
329                    serde_json::json!(request.permissions),
330                );
331            emit_security_event(event).await;
332
333            Ok(Json(ReviewActionResponse {
334                review_id,
335                user_id: request.user_id,
336                status: "permissions_updated".to_string(),
337                timestamp: chrono::Utc::now(),
338                message: Some("Permissions updated successfully".to_string()),
339            }))
340        }
341        Err(e) => {
342            error!("Failed to update permissions: {}", e);
343            Err(StatusCode::BAD_REQUEST)
344        }
345    }
346}
347
348/// Get review report
349///
350/// GET /api/v1/security/access-reviews/{review_id}/report
351pub async fn get_review_report(
352    State(state): State<AccessReviewState>,
353    Path(review_id): Path<String>,
354) -> Result<Json<serde_json::Value>, StatusCode> {
355    let service = state.service.read().await;
356
357    let review = service.get_review(&review_id).ok_or_else(|| {
358        error!("Review {} not found", review_id);
359        StatusCode::NOT_FOUND
360    })?;
361
362    // Convert review to JSON report format
363    let report = serde_json::json!({
364        "review_id": review.review_id,
365        "review_date": review.review_date,
366        "review_type": format!("{:?}", review.review_type),
367        "status": format!("{:?}", review.status),
368        "total_items": review.total_items,
369        "items_reviewed": review.items_reviewed,
370        "findings": review.findings,
371        "actions_taken": review.actions_taken,
372        "pending_reviews": review.pending_approvals,
373        "next_review_date": review.next_review_date,
374    });
375
376    Ok(Json(report))
377}
378
379/// Start a new access review
380///
381/// POST /api/v1/security/access-reviews/start
382pub async fn start_review(
383    State(state): State<AccessReviewState>,
384    Json(request): Json<StartReviewRequest>,
385) -> Result<Json<ReviewDetailResponse>, StatusCode> {
386    let mut service = state.service.write().await;
387
388    // Start review based on type
389    let review_id = match request.review_type {
390        ReviewType::UserAccess => service.start_user_access_review().await.map_err(|e| {
391            error!("Failed to start user access review: {}", e);
392            StatusCode::INTERNAL_SERVER_ERROR
393        })?,
394        ReviewType::PrivilegedAccess => {
395            service.start_privileged_access_review().await.map_err(|e| {
396                error!("Failed to start privileged access review: {}", e);
397                StatusCode::INTERNAL_SERVER_ERROR
398            })?
399        }
400        ReviewType::ApiToken => service.start_token_review().await.map_err(|e| {
401            error!("Failed to start token review: {}", e);
402            StatusCode::INTERNAL_SERVER_ERROR
403        })?,
404        ReviewType::ResourceAccess => {
405            return Err(StatusCode::NOT_IMPLEMENTED);
406        }
407    };
408
409    info!("Started access review: {}", review_id);
410
411    // Get the review details
412    let review = service
413        .get_review(&review_id)
414        .ok_or_else(|| {
415            error!("Review {} not found after creation", review_id);
416            StatusCode::INTERNAL_SERVER_ERROR
417        })?
418        .clone();
419
420    // Emit security event
421    let event = SecurityEvent::new(SecurityEventType::ComplianceComplianceCheck, None, None)
422        .with_target(EventTarget {
423            resource_type: Some("access_review".to_string()),
424            resource_id: Some(review_id.clone()),
425            method: None,
426        })
427        .with_outcome(EventOutcome {
428            success: true,
429            reason: Some("Access review started".to_string()),
430        });
431    emit_security_event(event).await;
432
433    let items = service
434        .engine()
435        .get_review_items(&review_id)
436        .map(|items_map| items_map.values().cloned().collect());
437
438    Ok(Json(ReviewDetailResponse { review, items }))
439}
440
441/// Request to start a review
442#[derive(Debug, Deserialize)]
443pub struct StartReviewRequest {
444    /// Review type to start
445    pub review_type: ReviewType,
446}
447
448/// Create access review router
449pub fn access_review_router(state: AccessReviewState) -> axum::Router {
450    use axum::routing::{get, post};
451
452    axum::Router::new()
453        .route("/", get(list_reviews))
454        .route("/start", post(start_review))
455        .route("/{review_id}", get(get_review))
456        .route("/{review_id}/approve", post(approve_access))
457        .route("/{review_id}/revoke", post(revoke_access))
458        .route("/{review_id}/update-permissions", post(update_permissions))
459        .route("/{review_id}/report", get(get_review_report))
460        .with_state(state)
461}