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