Skip to main content

backbone_authorization/
middleware.rs

1//! Axum middleware for authorization
2//!
3//! Provides Axum middleware integration for authorization system.
4
5use axum::{
6    extract::Request,
7    http::StatusCode,
8    response::{IntoResponse, Response},
9};
10use std::sync::Arc;
11use tracing::{debug, error};
12use tower_layer::Layer;
13
14use crate::service::*;
15use crate::traits::*;
16use crate::types::*;
17
18/// Authorization state stored in request extensions
19#[derive(Clone, Debug)]
20pub struct AuthState {
21    /// The authenticated user, when the request carries one.
22    pub user_id: Option<String>,
23}
24
25/// Authorization middleware layer
26#[derive(Clone)]
27pub struct AuthorizationLayer {
28    service: Arc<AuthorizationService>,
29}
30
31impl AuthorizationLayer {
32    /// A layer that authorizes requests through `service`.
33    pub fn new(service: Arc<AuthorizationService>) -> Self {
34        Self { service }
35    }
36}
37
38impl<S> Layer<S> for AuthorizationLayer
39where
40    S: Clone + Send + 'static,
41{
42    type Service = AuthorizationMiddleware<S>;
43
44    fn layer(&self, inner: S) -> Self::Service {
45        AuthorizationMiddleware {
46            inner,
47            service: self.service.clone(),
48        }
49    }
50}
51
52/// Authorization middleware service
53pub struct AuthorizationMiddleware<S> {
54    inner: S,
55    service: Arc<AuthorizationService>,
56}
57
58impl<S> Clone for AuthorizationMiddleware<S>
59where
60    S: Clone,
61{
62    fn clone(&self) -> Self {
63        Self {
64            inner: self.inner.clone(),
65            service: self.service.clone(),
66        }
67    }
68}
69
70impl<S> tower::Service<Request> for AuthorizationMiddleware<S>
71where
72    S: tower::Service<Request> + Clone + Send + 'static,
73    S::Response: IntoResponse,
74    S::Error: Into<std::io::Error>,
75    S::Future: Send,
76{
77    type Response = Response;
78    type Error = std::io::Error;
79    type Future = futures::future::BoxFuture<'static, Result<Response, Self::Error>>;
80
81    fn poll_ready(
82        &mut self,
83        cx: &mut std::task::Context<'_>,
84    ) -> std::task::Poll<Result<(), Self::Error>> {
85        self.inner.poll_ready(cx).map_err(Into::into)
86    }
87
88    fn call(&mut self, req: Request) -> Self::Future {
89        let service = self.service.clone();
90        let mut inner = self.inner.clone();
91
92        Box::pin(async move {
93            // Get auth header
94            let auth_header = req
95                .headers()
96                .get("authorization")
97                .and_then(|h| h.to_str().ok())
98                .and_then(|h| {
99                    if h.starts_with("Bearer ") {
100                        Some(h[7..].to_string())
101                    } else {
102                        None
103                    }
104                });
105
106            // Skip authorization for public endpoints
107            let path = req.uri().path();
108            if is_public_endpoint(path) {
109                debug!("Public endpoint '{}', skipping authorization", path);
110                return inner
111                    .call(req)
112                    .await
113                    .map(IntoResponse::into_response)
114                    .map_err(Into::into);
115            }
116
117            // Get user_id from auth header
118            let user_id = match auth_header {
119                Some(uid) => uid,
120                None => {
121                    error!("No user_id found for protected endpoint '{}'", path);
122                    return Ok(StatusCode::UNAUTHORIZED.into_response());
123                }
124            };
125
126            // Create authorization request
127            let auth_request = AuthorizationRequest {
128                user: AuthUser {
129                    user_id: user_id.clone(),
130                    username: user_id.clone(),
131                    roles: vec![],
132                    permissions: vec![],
133                    expires_at: None,
134                },
135                resource: to_resource_type(path),
136                action: to_action(req.method()),
137                resource_id: None,
138            };
139
140            // Check authorization
141            match service.check_authorization(auth_request).await {
142                Ok(response) if response.allowed => {
143                    debug!("Authorization passed for user '{}': {:?}", user_id, response.checks);
144                    inner
145                        .call(req)
146                        .await
147                        .map(IntoResponse::into_response)
148                        .map_err(Into::into)
149                }
150                Ok(_) => {
151                    debug!("Authorization failed for user '{}'", user_id);
152                    Ok(StatusCode::FORBIDDEN.into_response())
153                }
154                Err(e) => {
155                    error!("Authorization error: {:?}", e);
156                    Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
157                }
158            }
159        })
160    }
161}
162
163/// Convert HTTP method to action
164fn to_action(method: &axum::http::Method) -> Action {
165    match method {
166        &axum::http::Method::GET => Action::Read,
167        &axum::http::Method::POST => Action::Create,
168        &axum::http::Method::PUT => Action::Update,
169        &axum::http::Method::DELETE => Action::Delete,
170        &axum::http::Method::PATCH => Action::Update,
171        _ => Action::Read,
172    }
173}
174
175/// Convert path to resource type
176fn to_resource_type(path: &str) -> Resource {
177    // Extract resource from path (e.g., /api/v1/users/123 -> users)
178    let parts: Vec<&str> = path
179        .trim_end_matches('/')
180        .split('/')
181        .collect();
182
183    // Skip first empty part and api version
184    let resource = parts.get(2).unwrap_or(&"unknown");
185    match *resource {
186        "users" => Resource::User,
187        "roles" => Resource::Role,
188        "permissions" => Resource::Permission,
189        "settings" => Resource::Settings,
190        _ => Resource::User,
191    }
192}
193
194/// Check if path is a public endpoint (no authorization required)
195fn is_public_endpoint(path: &str) -> bool {
196    let public_paths = [
197        "/health",
198        "/api/v1/auth/login",
199        "/api/v1/auth/register",
200        "/api/v1/auth/forgot-password",
201        "/docs",
202        "/",
203    ];
204
205    public_paths
206        .iter()
207        .any(|p| if *p == "/" { path == "/" } else { path == *p || path.starts_with(p) })
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_is_public_endpoint() {
216        assert!(is_public_endpoint("/health"));
217        assert!(is_public_endpoint("/api/v1/auth/login"));
218        assert!(is_public_endpoint("/api/v1/auth/register"));
219        assert!(!is_public_endpoint("/api/v1/users"));
220        assert!(!is_public_endpoint("/api/v1/roles"));
221    }
222
223    #[test]
224    fn test_to_action() {
225        assert!(matches!(to_action(&axum::http::Method::GET), Action::Read));
226        assert!(matches!(to_action(&axum::http::Method::POST), Action::Create));
227        assert!(matches!(to_action(&axum::http::Method::PUT), Action::Update));
228        assert!(matches!(to_action(&axum::http::Method::DELETE), Action::Delete));
229    }
230}