use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
};
use mockforge_core::security::{
access_review::{AccessReview, ReviewType, UserReviewItem},
access_review_service::AccessReviewService,
emit_security_event, EventActor, EventOutcome, EventTarget, SecurityEvent, SecurityEventType,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info};
use uuid::Uuid;
use crate::handlers::auth_helpers::{extract_user_id_with_fallback, OptionalAuthClaims};
#[derive(Clone)]
pub struct AccessReviewState {
pub service: Arc<RwLock<AccessReviewService>>,
}
#[derive(Debug, Deserialize)]
pub struct ApproveAccessRequest {
pub user_id: Uuid,
pub approved: bool,
pub justification: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct RevokeAccessRequest {
pub user_id: Uuid,
pub reason: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdatePermissionsRequest {
pub user_id: Uuid,
pub roles: Vec<String>,
pub permissions: Vec<String>,
pub reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ReviewListResponse {
pub reviews: Vec<ReviewSummary>,
}
#[derive(Debug, Serialize)]
pub struct ReviewSummary {
pub review_id: String,
pub review_type: String,
pub status: String,
pub due_date: chrono::DateTime<chrono::Utc>,
pub items_count: u32,
pub pending_approvals: u32,
}
#[derive(Debug, Serialize)]
pub struct ReviewDetailResponse {
#[serde(flatten)]
pub review: AccessReview,
pub items: Option<Vec<UserReviewItem>>,
}
#[derive(Debug, Serialize)]
pub struct ReviewActionResponse {
pub review_id: String,
pub user_id: Uuid,
pub status: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub message: Option<String>,
}
pub async fn list_reviews(
State(state): State<AccessReviewState>,
) -> Result<Json<ReviewListResponse>, StatusCode> {
let service = state.service.read().await;
let reviews = service.get_all_reviews();
let summaries: Vec<ReviewSummary> = reviews
.iter()
.map(|review| ReviewSummary {
review_id: review.review_id.clone(),
review_type: format!("{:?}", review.review_type),
status: format!("{:?}", review.status),
due_date: review.due_date,
items_count: review.total_items,
pending_approvals: review.pending_approvals,
})
.collect();
Ok(Json(ReviewListResponse { reviews: summaries }))
}
pub async fn get_review(
State(state): State<AccessReviewState>,
Path(review_id): Path<String>,
) -> Result<Json<ReviewDetailResponse>, StatusCode> {
let service = state.service.read().await;
let review = service
.get_review(&review_id)
.ok_or_else(|| {
error!("Review {} not found", review_id);
StatusCode::NOT_FOUND
})?
.clone();
let items = service
.engine()
.get_review_items(&review_id)
.map(|items_map| items_map.values().cloned().collect());
Ok(Json(ReviewDetailResponse { review, items }))
}
pub async fn approve_access(
State(state): State<AccessReviewState>,
Path(review_id): Path<String>,
claims: OptionalAuthClaims,
Json(request): Json<ApproveAccessRequest>,
) -> Result<Json<ReviewActionResponse>, StatusCode> {
let mut service = state.service.write().await;
let approver_id = extract_user_id_with_fallback(&claims);
match service
.approve_user_access(&review_id, request.user_id, approver_id, request.justification)
.await
{
Ok(()) => {
info!("Access approved for user {} in review {}", request.user_id, review_id);
let event = SecurityEvent::new(SecurityEventType::AuthzAccessGranted, None, None)
.with_actor(EventActor {
user_id: Some(approver_id.to_string()),
username: None,
ip_address: None,
user_agent: None,
})
.with_target(EventTarget {
resource_type: Some("access_review".to_string()),
resource_id: Some(review_id.clone()),
method: None,
})
.with_outcome(EventOutcome {
success: true,
reason: Some("Access approved in review".to_string()),
})
.with_metadata("user_id".to_string(), serde_json::json!(request.user_id));
emit_security_event(event).await;
Ok(Json(ReviewActionResponse {
review_id,
user_id: request.user_id,
status: "approved".to_string(),
timestamp: chrono::Utc::now(),
message: Some("Access approved successfully".to_string()),
}))
}
Err(e) => {
error!("Failed to approve access: {}", e);
Err(StatusCode::BAD_REQUEST)
}
}
}
pub async fn revoke_access(
State(state): State<AccessReviewState>,
Path(review_id): Path<String>,
claims: OptionalAuthClaims,
Json(request): Json<RevokeAccessRequest>,
) -> Result<Json<ReviewActionResponse>, StatusCode> {
let mut service = state.service.write().await;
let revoker_id = extract_user_id_with_fallback(&claims);
match service
.revoke_user_access(&review_id, request.user_id, revoker_id, request.reason.clone())
.await
{
Ok(()) => {
info!("Access revoked for user {} in review {}", request.user_id, review_id);
let event = SecurityEvent::new(SecurityEventType::AccessUserSuspended, None, None)
.with_actor(EventActor {
user_id: Some(revoker_id.to_string()),
username: None,
ip_address: None,
user_agent: None,
})
.with_target(EventTarget {
resource_type: Some("access_review".to_string()),
resource_id: Some(review_id.clone()),
method: None,
})
.with_outcome(EventOutcome {
success: true,
reason: Some(request.reason.clone()),
})
.with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
.with_metadata("review_id".to_string(), serde_json::json!(review_id));
emit_security_event(event).await;
Ok(Json(ReviewActionResponse {
review_id,
user_id: request.user_id,
status: "revoked".to_string(),
timestamp: chrono::Utc::now(),
message: Some(format!("Access revoked: {}", request.reason)),
}))
}
Err(e) => {
error!("Failed to revoke access: {}", e);
Err(StatusCode::BAD_REQUEST)
}
}
}
pub async fn update_permissions(
State(state): State<AccessReviewState>,
Path(review_id): Path<String>,
claims: OptionalAuthClaims,
Json(request): Json<UpdatePermissionsRequest>,
) -> Result<Json<ReviewActionResponse>, StatusCode> {
let mut service = state.service.write().await;
let updater_id = extract_user_id_with_fallback(&claims);
match service
.update_user_permissions(
&review_id,
request.user_id,
updater_id,
request.roles.clone(),
request.permissions.clone(),
request.reason.clone(),
)
.await
{
Ok(()) => {
info!("Permissions updated for user {} in review {}", request.user_id, review_id);
let event = SecurityEvent::new(SecurityEventType::AuthzPermissionChanged, None, None)
.with_actor(EventActor {
user_id: Some(updater_id.to_string()),
username: None,
ip_address: None,
user_agent: None,
})
.with_target(EventTarget {
resource_type: Some("access_review".to_string()),
resource_id: Some(review_id.clone()),
method: None,
})
.with_outcome(EventOutcome {
success: true,
reason: request.reason.clone(),
})
.with_metadata("user_id".to_string(), serde_json::json!(request.user_id))
.with_metadata("review_id".to_string(), serde_json::json!(review_id))
.with_metadata("new_roles".to_string(), serde_json::json!(request.roles))
.with_metadata(
"new_permissions".to_string(),
serde_json::json!(request.permissions),
);
emit_security_event(event).await;
Ok(Json(ReviewActionResponse {
review_id,
user_id: request.user_id,
status: "permissions_updated".to_string(),
timestamp: chrono::Utc::now(),
message: Some("Permissions updated successfully".to_string()),
}))
}
Err(e) => {
error!("Failed to update permissions: {}", e);
Err(StatusCode::BAD_REQUEST)
}
}
}
pub async fn get_review_report(
State(state): State<AccessReviewState>,
Path(review_id): Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let service = state.service.read().await;
let review = service.get_review(&review_id).ok_or_else(|| {
error!("Review {} not found", review_id);
StatusCode::NOT_FOUND
})?;
let report = serde_json::json!({
"review_id": review.review_id,
"review_date": review.review_date,
"review_type": format!("{:?}", review.review_type),
"status": format!("{:?}", review.status),
"total_items": review.total_items,
"items_reviewed": review.items_reviewed,
"findings": review.findings,
"actions_taken": review.actions_taken,
"pending_reviews": review.pending_approvals,
"next_review_date": review.next_review_date,
});
Ok(Json(report))
}
pub async fn start_review(
State(state): State<AccessReviewState>,
Json(request): Json<StartReviewRequest>,
) -> Result<Json<ReviewDetailResponse>, StatusCode> {
let mut service = state.service.write().await;
let review_id = match request.review_type {
ReviewType::UserAccess => service.start_user_access_review().await.map_err(|e| {
error!("Failed to start user access review: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?,
ReviewType::PrivilegedAccess => {
service.start_privileged_access_review().await.map_err(|e| {
error!("Failed to start privileged access review: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?
}
ReviewType::ApiToken => service.start_token_review().await.map_err(|e| {
error!("Failed to start token review: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?,
ReviewType::ResourceAccess => {
service.start_resource_access_review(Vec::new()).await.map_err(|e| {
error!("Failed to start resource access review: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?
}
};
info!("Started access review: {}", review_id);
let review = service
.get_review(&review_id)
.ok_or_else(|| {
error!("Review {} not found after creation", review_id);
StatusCode::INTERNAL_SERVER_ERROR
})?
.clone();
let event = SecurityEvent::new(SecurityEventType::ComplianceComplianceCheck, None, None)
.with_target(EventTarget {
resource_type: Some("access_review".to_string()),
resource_id: Some(review_id.clone()),
method: None,
})
.with_outcome(EventOutcome {
success: true,
reason: Some("Access review started".to_string()),
});
emit_security_event(event).await;
let items = service
.engine()
.get_review_items(&review_id)
.map(|items_map| items_map.values().cloned().collect());
Ok(Json(ReviewDetailResponse { review, items }))
}
#[derive(Debug, Deserialize)]
pub struct StartReviewRequest {
pub review_type: ReviewType,
}
pub fn access_review_router(state: AccessReviewState) -> axum::Router {
use axum::routing::{get, post};
axum::Router::new()
.route("/", get(list_reviews))
.route("/start", post(start_review))
.route("/{review_id}", get(get_review))
.route("/{review_id}/approve", post(approve_access))
.route("/{review_id}/revoke", post(revoke_access))
.route("/{review_id}/update-permissions", post(update_permissions))
.route("/{review_id}/report", get(get_review_report))
.with_state(state)
}