Skip to main content

mockforge_intelligence/handlers/
risk_simulation.rs

1//! Programmatic authentication risk simulation routes.
2//!
3//! Internal API only; the consuming application supplies its existing engine.
4
5use async_trait::async_trait;
6use axum::{extract::State, http::StatusCode, response::Json};
7use mockforge_foundation::auth_risk_types::RiskAssessment;
8use serde::Deserialize;
9use std::collections::HashMap;
10
11/// Risk-engine operations used by the simulation routes.
12#[async_trait]
13pub trait RiskSimulationEngine: Send + Sync {
14    /// Set or remove the existing score override.
15    async fn set_simulated_risk(&self, user_id: String, score: Option<f64>);
16    /// Set the existing per-user factor overrides.
17    async fn set_simulated_factors(&self, user_id: String, factors: HashMap<String, f64>);
18    /// Clear score and factor overrides for one user.
19    async fn clear_simulated_risk(&self, user_id: &str);
20    /// Assess risk using the application's current configuration and overrides.
21    async fn assess_risk(&self, user_id: &str, factors: &HashMap<String, f64>) -> RiskAssessment;
22}
23
24/// State supplied by the application without exposing its authentication module.
25pub trait RiskSimulationStateProvider: Clone + Send + Sync + 'static {
26    /// The application's existing risk engine.
27    type Engine: RiskSimulationEngine;
28    /// Access that engine, preserving shared state with authentication consumers.
29    fn risk_engine(&self) -> &Self::Engine;
30}
31
32/// Set risk score request
33#[derive(Debug, Deserialize)]
34pub struct SetRiskScoreRequest {
35    /// User ID
36    pub user_id: String,
37    /// Risk score (0.0 - 1.0)
38    pub risk_score: f64,
39}
40
41/// Set risk factors request
42#[derive(Debug, Deserialize)]
43pub struct SetRiskFactorsRequest {
44    /// User ID
45    pub user_id: String,
46    /// Risk factors
47    pub risk_factors: HashMap<String, f64>,
48}
49
50/// Trigger MFA request
51#[derive(Debug, Deserialize)]
52pub struct TriggerMfaRequest {
53    /// User ID
54    pub user_id: String,
55    /// MFA type
56    pub mfa_type: String,
57}
58
59/// Block user request
60#[derive(Debug, Deserialize)]
61pub struct BlockUserRequest {
62    /// User ID
63    pub user_id: String,
64    /// Reason
65    pub reason: String,
66}
67
68/// Set simulated risk score
69pub async fn set_risk_score<S: RiskSimulationStateProvider>(
70    State(state): State<S>,
71    Json(request): Json<SetRiskScoreRequest>,
72) -> Result<Json<serde_json::Value>, StatusCode> {
73    state
74        .risk_engine()
75        .set_simulated_risk(request.user_id.clone(), Some(request.risk_score))
76        .await;
77
78    Ok(Json(serde_json::json!({
79        "success": true,
80        "user_id": request.user_id,
81        "risk_score": request.risk_score
82    })))
83}
84
85/// Set simulated risk factors
86pub async fn set_risk_factors<S: RiskSimulationStateProvider>(
87    State(state): State<S>,
88    Json(request): Json<SetRiskFactorsRequest>,
89) -> Result<Json<serde_json::Value>, StatusCode> {
90    state
91        .risk_engine()
92        .set_simulated_factors(request.user_id.clone(), request.risk_factors.clone())
93        .await;
94
95    Ok(Json(serde_json::json!({
96        "success": true,
97        "user_id": request.user_id,
98        "risk_factors": request.risk_factors
99    })))
100}
101
102/// Clear simulated risk
103pub async fn clear_risk<S: RiskSimulationStateProvider>(
104    State(state): State<S>,
105    axum::extract::Path(user_id): axum::extract::Path<String>,
106) -> Result<Json<serde_json::Value>, StatusCode> {
107    state.risk_engine().clear_simulated_risk(&user_id).await;
108
109    Ok(Json(serde_json::json!({
110        "success": true,
111        "user_id": user_id,
112        "message": "Simulated risk cleared"
113    })))
114}
115
116/// Trigger MFA prompt
117pub async fn trigger_mfa<S: RiskSimulationStateProvider>(
118    State(state): State<S>,
119    Json(request): Json<TriggerMfaRequest>,
120) -> Result<Json<serde_json::Value>, StatusCode> {
121    // Set risk score high enough to trigger MFA
122    state.risk_engine().set_simulated_risk(request.user_id.clone(), Some(0.8)).await;
123
124    Ok(Json(serde_json::json!({
125        "success": true,
126        "user_id": request.user_id,
127        "mfa_type": request.mfa_type,
128        "message": "MFA prompt triggered"
129    })))
130}
131
132/// Block user login
133pub async fn block_user<S: RiskSimulationStateProvider>(
134    State(state): State<S>,
135    Json(request): Json<BlockUserRequest>,
136) -> Result<Json<serde_json::Value>, StatusCode> {
137    // Set risk score high enough to block
138    state
139        .risk_engine()
140        .set_simulated_risk(request.user_id.clone(), Some(0.95))
141        .await;
142
143    Ok(Json(serde_json::json!({
144        "success": true,
145        "user_id": request.user_id,
146        "reason": request.reason,
147        "message": "User login blocked"
148    })))
149}
150
151/// Get risk assessment for user
152pub async fn get_risk_assessment<S: RiskSimulationStateProvider>(
153    State(state): State<S>,
154    axum::extract::Path(user_id): axum::extract::Path<String>,
155) -> Json<serde_json::Value> {
156    let risk_factors = HashMap::new();
157    let assessment = state.risk_engine().assess_risk(&user_id, &risk_factors).await;
158
159    Json(serde_json::json!({
160        "user_id": user_id,
161        "risk_score": assessment.risk_score,
162        "risk_factors": assessment.risk_factors,
163        "recommended_action": assessment.recommended_action
164    }))
165}
166
167/// Create risk simulation router
168pub fn risk_simulation_router<S: RiskSimulationStateProvider>(state: S) -> axum::Router {
169    use axum::routing::{delete, get, post};
170
171    axum::Router::new()
172        .route("/risk/simulate", post(set_risk_score::<S>))
173        .route("/risk/factors", post(set_risk_factors::<S>))
174        .route("/risk/clear/{user_id}", delete(clear_risk::<S>))
175        .route("/risk/trigger-mfa", post(trigger_mfa::<S>))
176        .route("/risk/block", post(block_user::<S>))
177        .route("/risk/assessment/{user_id}", get(get_risk_assessment::<S>))
178        .with_state(state)
179}