Skip to main content

actix_cloud/
router.rs

1//! Declarative router with per-route auth and CSRF configuration.
2//!
3//! Build a `Vec<Router>` and pass it to [`build_router`]; the returned closure is
4//! configured on a scope (`scope("/api").configure(build_router(...))`). Each route
5//! runs the [`Checker`] auth guard first, then CSRF (feature `csrf`).
6use std::{
7    fmt::Debug,
8    future::{ready, Ready},
9    rc::Rc,
10};
11
12#[cfg(feature = "csrf")]
13use actix_web::HttpMessage;
14use actix_web::{
15    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
16    web::ServiceConfig,
17    Route,
18};
19use anyhow::Result;
20use async_trait::async_trait;
21use futures::future::LocalBoxFuture;
22
23#[cfg(feature = "csrf")]
24/// Configure routes on a scope, wrapping each route with the CSRF middleware
25/// (innermost) and the auth guard.
26pub fn build_router<F, Fut>(
27    router: Vec<Router>,
28    csrf: crate::csrf::Middleware<F>,
29) -> impl FnOnce(&mut ServiceConfig)
30where
31    F: Fn(actix_web::HttpRequest, String) -> Fut + 'static,
32    Fut: futures::Future<Output = Result<bool, actix_web::Error>>,
33{
34    move |cfg| {
35        for i in router {
36            if !i.path.is_empty() {
37                cfg.route(
38                    &i.path,
39                    i.route.wrap(csrf.clone()).wrap(RouterGuard {
40                        checker: i.checker,
41                        csrf: i.csrf,
42                    }),
43                );
44            }
45        }
46    }
47}
48
49#[cfg(not(feature = "csrf"))]
50/// Configure routes on a scope, wrapping each route with the auth guard.
51pub fn build_router(router: Vec<Router>) -> impl FnOnce(&mut ServiceConfig) {
52    |cfg| {
53        for i in router {
54            if !i.path.is_empty() {
55                cfg.route(&i.path, i.route.wrap(RouterGuard { checker: i.checker }));
56            }
57        }
58    }
59}
60
61/// Per-route permission checker.
62///
63/// Return `Ok(false)` to reject the request with 403; an `Err` yields 500. `?Send` is
64/// required because actix runs each worker single-threaded.
65#[async_trait(?Send)]
66pub trait Checker {
67    async fn check(&self, req: &mut ServiceRequest) -> Result<bool>;
68}
69
70#[cfg(feature = "csrf")]
71/// Per-route CSRF enforcement level (see [`crate::csrf`]).
72///
73/// By default checks apply to all unsafe methods; `ForceHeader`/`ForceParam` check on
74/// every method, and `Disabled` never checks.
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76#[derive(Clone, Copy, enum_as_inner::EnumAsInner)]
77pub enum CSRFType {
78    Header,
79    Param,
80    ForceHeader,
81    ForceParam,
82    Disabled,
83}
84
85/// A route entry for [`build_router`].
86pub struct Router {
87    /// Route path registered on the scope.
88    pub path: String,
89    /// Actix-web route (method + handler).
90    pub route: Route,
91    /// Auth checker; `None` skips the auth check for this route.
92    pub checker: Option<Rc<dyn Checker>>,
93    #[cfg(feature = "csrf")]
94    /// CSRF enforcement level for this route.
95    pub csrf: CSRFType,
96}
97
98pub(crate) struct RouterGuard {
99    checker: Option<Rc<dyn Checker>>,
100    #[cfg(feature = "csrf")]
101    csrf: CSRFType,
102}
103
104impl<S, B> Transform<S, ServiceRequest> for RouterGuard
105where
106    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
107    S::Future: 'static,
108    B: 'static + Debug,
109{
110    type Response = ServiceResponse<B>;
111    type Error = actix_web::Error;
112    type InitError = ();
113    type Transform = RouterGuardMiddleware<S>;
114    type Future = Ready<Result<Self::Transform, Self::InitError>>;
115
116    fn new_transform(&self, service: S) -> Self::Future {
117        ready(Ok(RouterGuardMiddleware {
118            service: Rc::new(service),
119            checker: self.checker.clone(),
120            #[cfg(feature = "csrf")]
121            csrf: self.csrf,
122        }))
123    }
124}
125
126pub(crate) struct RouterGuardMiddleware<S> {
127    service: Rc<S>,
128    checker: Option<Rc<dyn Checker>>,
129    #[cfg(feature = "csrf")]
130    csrf: CSRFType,
131}
132
133impl<S, B> Service<ServiceRequest> for RouterGuardMiddleware<S>
134where
135    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
136    S::Future: 'static,
137    B: 'static + Debug,
138{
139    type Response = ServiceResponse<B>;
140    type Error = actix_web::Error;
141    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
142
143    forward_ready!(service);
144
145    fn call(&self, mut req: ServiceRequest) -> Self::Future {
146        let srv = self.service.clone();
147        let checker = self.checker.clone();
148        #[cfg(feature = "csrf")]
149        req.extensions_mut().insert(self.csrf);
150        Box::pin(async move {
151            if let Some(checker) = checker {
152                match checker.check(&mut req).await {
153                    Ok(ok) => {
154                        if ok {
155                            srv.call(req).await
156                        } else {
157                            Err(actix_web::error::ErrorForbidden("Checker failed"))
158                        }
159                    }
160                    Err(e) => Err(actix_web::error::ErrorInternalServerError(e)),
161                }
162            } else {
163                srv.call(req).await
164            }
165        })
166    }
167}