koprogo_api/infrastructure/web/middleware/
mod.rs1pub mod scope_guard;
3pub use scope_guard::{AcpScope, ScopeGuard, ScopeGuardError};
4
5pub mod community_access_guard;
8pub use community_access_guard::CommunityAccessGuard;
9
10use crate::infrastructure::web::app_state::AppState;
11use actix_web::{
12 body::MessageBody,
13 dev::{forward_ready, Payload, Service, ServiceRequest, ServiceResponse, Transform},
14 error::ErrorUnauthorized,
15 http::StatusCode,
16 web, Error, FromRequest, HttpRequest, HttpResponse,
17};
18use std::collections::HashMap;
19use std::future::{ready, Future, Ready};
20use std::pin::Pin;
21use std::sync::{Arc, Mutex};
22use std::time::{Duration, Instant};
23use uuid::Uuid;
24
25#[derive(Debug, Clone)]
39pub struct AuthenticatedUser {
40 pub user_id: Uuid,
41 pub email: String,
42 pub role: String,
43 pub role_id: Option<Uuid>,
44 pub organization_id: Option<Uuid>,
45}
46
47impl AuthenticatedUser {
48 pub fn require_organization(&self) -> Result<Uuid, Error> {
50 self.organization_id
51 .ok_or_else(|| ErrorUnauthorized("User does not belong to an organization"))
52 }
53
54 pub fn is_superadmin(&self) -> bool {
56 self.role == "superadmin"
57 }
58
59 pub fn effective_org_filter(&self) -> Option<Uuid> {
63 if self.is_superadmin() {
64 None
65 } else {
66 self.organization_id
67 }
68 }
69
70 pub fn verify_org_access(&self, resource_org_id: Uuid) -> Result<(), String> {
74 if self.is_superadmin() {
75 return Ok(());
76 }
77 match self.organization_id {
78 Some(user_org_id) if user_org_id == resource_org_id => Ok(()),
79 Some(_) => Err("Access denied: resource belongs to another organization".to_string()),
80 None => Err("User does not belong to an organization".to_string()),
81 }
82 }
83}
84
85impl FromRequest for AuthenticatedUser {
86 type Error = Error;
87 type Future = Ready<Result<Self, Self::Error>>;
88
89 fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
90 let app_state = match req.app_data::<web::Data<AppState>>() {
92 Some(state) => state,
93 None => return ready(Err(ErrorUnauthorized("Internal server error"))),
94 };
95
96 let auth_header = match req.headers().get("Authorization") {
98 Some(header) => match header.to_str() {
99 Ok(s) => s,
100 Err(_) => return ready(Err(ErrorUnauthorized("Invalid authorization header"))),
101 },
102 None => return ready(Err(ErrorUnauthorized("Missing authorization header"))),
103 };
104
105 let token = auth_header.trim_start_matches("Bearer ").trim();
107
108 match app_state.auth_use_cases.verify_token(token) {
110 Ok(claims) => {
111 match Uuid::parse_str(&claims.sub) {
113 Ok(user_id) => ready(Ok(AuthenticatedUser {
114 user_id,
115 email: claims.email,
116 role: claims.role,
117 role_id: claims.role_id,
118 organization_id: claims.organization_id,
119 })),
120 Err(_) => ready(Err(ErrorUnauthorized("Invalid user ID in token"))),
121 }
122 }
123 Err(e) => ready(Err(ErrorUnauthorized(e))),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy)]
146pub struct OrganizationId(pub Uuid);
147
148impl FromRequest for OrganizationId {
149 type Error = Error;
150 type Future = Ready<Result<Self, Self::Error>>;
151
152 fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
153 let user_future = AuthenticatedUser::from_request(req, payload);
155
156 match user_future.into_inner() {
158 Ok(user) => match user.organization_id {
159 Some(org_id) => ready(Ok(OrganizationId(org_id))),
160 None => ready(Err(ErrorUnauthorized(
161 "User does not belong to an organization",
162 ))),
163 },
164 Err(e) => ready(Err(e)),
165 }
166 }
167}
168
169#[derive(Clone, Debug)]
175pub struct GdprRateLimitConfig {
176 pub max_requests: usize,
178 pub window_duration: Duration,
180}
181
182impl Default for GdprRateLimitConfig {
183 fn default() -> Self {
184 Self {
185 max_requests: 10,
186 window_duration: Duration::from_secs(3600), }
188 }
189}
190
191#[derive(Clone)]
193pub struct GdprRateLimitState {
194 state: Arc<Mutex<HashMap<String, (usize, Instant)>>>,
195 config: GdprRateLimitConfig,
196}
197
198impl GdprRateLimitState {
199 pub fn new(config: GdprRateLimitConfig) -> Self {
200 Self {
201 state: Arc::new(Mutex::new(HashMap::new())),
202 config,
203 }
204 }
205
206 pub fn check_rate_limit(&self, user_id: &str) -> Result<(), String> {
208 let mut state = self.state.lock().unwrap();
209 let now = Instant::now();
210 let entry = state.entry(user_id.to_string()).or_insert((0, now));
211 let (count, window_start) = entry;
212
213 if now.duration_since(*window_start) > self.config.window_duration {
215 *count = 0;
216 *window_start = now;
217 }
218
219 if *count >= self.config.max_requests {
221 let reset_in = self
222 .config
223 .window_duration
224 .saturating_sub(now.duration_since(*window_start));
225 return Err(format!(
226 "Rate limit exceeded. Try again in {} seconds.",
227 reset_in.as_secs()
228 ));
229 }
230
231 *count += 1;
232 Ok(())
233 }
234}
235
236#[derive(Clone)]
242pub struct GdprRateLimit {
243 state: GdprRateLimitState,
244}
245
246impl GdprRateLimit {
247 pub fn new(config: GdprRateLimitConfig) -> Self {
248 Self {
249 state: GdprRateLimitState::new(config),
250 }
251 }
252}
253
254impl<S, B> Transform<S, ServiceRequest> for GdprRateLimit
255where
256 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
257 S::Future: 'static,
258 B: MessageBody + 'static,
259{
260 type Response = ServiceResponse<actix_web::body::EitherBody<B>>;
261 type Error = Error;
262 type InitError = ();
263 type Transform = GdprRateLimitMiddleware<S>;
264 type Future = Ready<Result<Self::Transform, Self::InitError>>;
265
266 fn new_transform(&self, service: S) -> Self::Future {
267 ready(Ok(GdprRateLimitMiddleware {
268 service: Arc::new(service),
269 state: self.state.clone(),
270 }))
271 }
272}
273
274pub struct GdprRateLimitMiddleware<S> {
275 service: Arc<S>,
276 state: GdprRateLimitState,
277}
278
279impl<S, B> Service<ServiceRequest> for GdprRateLimitMiddleware<S>
280where
281 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
282 S::Future: 'static,
283 B: MessageBody + 'static,
284{
285 type Response = ServiceResponse<actix_web::body::EitherBody<B>>;
286 type Error = Error;
287 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
288
289 forward_ready!(service);
290
291 fn call(&self, req: ServiceRequest) -> Self::Future {
292 let path = req.path().to_string();
293
294 let is_gdpr_endpoint =
296 path.starts_with("/api/v1/gdpr") || path.starts_with("/api/v1/admin/gdpr");
297
298 if !is_gdpr_endpoint {
299 let fut = self.service.call(req);
300 return Box::pin(async move { fut.await.map(|res| res.map_into_left_body()) });
301 }
302
303 let user_id = match req.app_data::<web::Data<AppState>>() {
305 Some(app_state) => {
306 let auth_header = match req.headers().get("Authorization") {
308 Some(header) => match header.to_str() {
309 Ok(s) => s.to_string(),
310 Err(_) => {
311 let fut = self.service.call(req);
313 return Box::pin(async move {
314 fut.await.map(|res| res.map_into_left_body())
315 });
316 }
317 },
318 None => {
319 let fut = self.service.call(req);
321 return Box::pin(
322 async move { fut.await.map(|res| res.map_into_left_body()) },
323 );
324 }
325 };
326
327 let token = auth_header.trim_start_matches("Bearer ").trim();
328
329 match app_state.auth_use_cases.verify_token(token) {
330 Ok(claims) => claims.sub,
331 Err(_) => {
332 let fut = self.service.call(req);
334 return Box::pin(
335 async move { fut.await.map(|res| res.map_into_left_body()) },
336 );
337 }
338 }
339 }
340 None => {
341 let fut = self.service.call(req);
342 return Box::pin(async move { fut.await.map(|res| res.map_into_left_body()) });
343 }
344 };
345
346 let state = self.state.clone();
348 let service = self.service.clone();
349
350 Box::pin(async move {
351 match state.check_rate_limit(&user_id) {
352 Ok(_) => {
353 service.call(req).await.map(|res| res.map_into_left_body())
355 }
356 Err(msg) => {
357 let retry_after = state.config.window_duration.as_secs().to_string();
359 let response = HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
360 .insert_header(("Retry-After", retry_after.clone()))
361 .json(serde_json::json!({
362 "error": msg,
363 "retry_after_seconds": state.config.window_duration.as_secs()
364 }));
365
366 Ok(req.into_response(response).map_into_right_body())
367 }
368 }
369 })
370 }
371}
372
373pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 20;
430
431#[derive(Clone, Copy, Debug)]
433pub struct ConcurrencyLimitConfig {
434 pub max_concurrent: usize,
437 pub retry_after_secs: u64,
439}
440
441impl Default for ConcurrencyLimitConfig {
442 fn default() -> Self {
443 let max_concurrent = std::env::var("MAX_CONCURRENT_REQUESTS")
444 .ok()
445 .and_then(|v| v.parse::<usize>().ok())
446 .filter(|v| *v > 0)
447 .unwrap_or(DEFAULT_MAX_CONCURRENT_REQUESTS);
448 Self {
449 max_concurrent,
450 retry_after_secs: 1,
451 }
452 }
453}
454
455#[derive(Clone)]
460pub struct RequestConcurrencyLimit {
461 semaphore: Arc<tokio::sync::Semaphore>,
462 retry_after_secs: u64,
463}
464
465impl RequestConcurrencyLimit {
466 pub fn new(config: ConcurrencyLimitConfig) -> Self {
467 Self {
468 semaphore: Arc::new(tokio::sync::Semaphore::new(config.max_concurrent)),
469 retry_after_secs: config.retry_after_secs,
470 }
471 }
472}
473
474impl<S, B> Transform<S, ServiceRequest> for RequestConcurrencyLimit
475where
476 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
477 S::Future: 'static,
478 B: MessageBody + 'static,
479{
480 type Response = ServiceResponse<actix_web::body::EitherBody<B>>;
481 type Error = Error;
482 type InitError = ();
483 type Transform = RequestConcurrencyLimitMiddleware<S>;
484 type Future = Ready<Result<Self::Transform, Self::InitError>>;
485
486 fn new_transform(&self, service: S) -> Self::Future {
487 ready(Ok(RequestConcurrencyLimitMiddleware {
488 service: Arc::new(service),
489 semaphore: self.semaphore.clone(),
490 retry_after_secs: self.retry_after_secs,
491 }))
492 }
493}
494
495pub struct RequestConcurrencyLimitMiddleware<S> {
496 service: Arc<S>,
497 semaphore: Arc<tokio::sync::Semaphore>,
498 retry_after_secs: u64,
499}
500
501impl<S, B> Service<ServiceRequest> for RequestConcurrencyLimitMiddleware<S>
502where
503 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
504 S::Future: 'static,
505 B: MessageBody + 'static,
506{
507 type Response = ServiceResponse<actix_web::body::EitherBody<B>>;
508 type Error = Error;
509 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
510
511 forward_ready!(service);
512
513 fn call(&self, req: ServiceRequest) -> Self::Future {
514 if req.path().ends_with("/health") {
517 let fut = self.service.call(req);
518 return Box::pin(async move { fut.await.map(|res| res.map_into_left_body()) });
519 }
520
521 match self.semaphore.clone().try_acquire_owned() {
522 Ok(permit) => {
523 let fut = self.service.call(req);
524 Box::pin(async move {
525 let res = fut.await;
526 drop(permit); res.map(|r| r.map_into_left_body())
528 })
529 }
530 Err(_) => {
531 let retry_after = self.retry_after_secs.to_string();
532 let response = HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
533 .insert_header(("Retry-After", retry_after))
534 .json(serde_json::json!({
535 "error": "server_busy",
536 "message": "Trop de requêtes en cours de traitement, réessayez sous peu.",
537 "retry_after_seconds": self.retry_after_secs,
538 }));
539 Box::pin(async move { Ok(req.into_response(response).map_into_right_body()) })
540 }
541 }
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_authenticated_user_require_organization() {
551 let user_with_org = AuthenticatedUser {
552 user_id: Uuid::new_v4(),
553 email: "test@example.com".to_string(),
554 role: "admin".to_string(),
555 role_id: None,
556 organization_id: Some(Uuid::new_v4()),
557 };
558
559 assert!(user_with_org.require_organization().is_ok());
560
561 let user_without_org = AuthenticatedUser {
562 user_id: Uuid::new_v4(),
563 email: "test@example.com".to_string(),
564 role: "admin".to_string(),
565 role_id: None,
566 organization_id: None,
567 };
568
569 assert!(user_without_org.require_organization().is_err());
570 }
571
572 #[test]
573 fn test_gdpr_rate_limit_config_default() {
574 let config = GdprRateLimitConfig::default();
575 assert_eq!(config.max_requests, 10);
576 assert_eq!(config.window_duration, Duration::from_secs(3600));
577 }
578
579 #[test]
580 fn test_gdpr_rate_limit_state_allows_within_limit() {
581 let config = GdprRateLimitConfig {
582 max_requests: 3,
583 window_duration: Duration::from_secs(60),
584 };
585 let state = GdprRateLimitState::new(config);
586
587 assert!(state.check_rate_limit("user1").is_ok());
588 assert!(state.check_rate_limit("user1").is_ok());
589 assert!(state.check_rate_limit("user1").is_ok());
590 }
591
592 #[test]
593 fn test_gdpr_rate_limit_state_blocks_exceeding_limit() {
594 let config = GdprRateLimitConfig {
595 max_requests: 2,
596 window_duration: Duration::from_secs(60),
597 };
598 let state = GdprRateLimitState::new(config);
599
600 assert!(state.check_rate_limit("user1").is_ok());
601 assert!(state.check_rate_limit("user1").is_ok());
602 let result = state.check_rate_limit("user1");
603 assert!(result.is_err());
604 assert!(result
605 .unwrap_err()
606 .contains("Rate limit exceeded. Try again in"));
607 }
608}