backbone_authorization/
middleware.rs1use 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#[derive(Clone, Debug)]
20pub struct AuthState {
21 pub user_id: Option<String>,
23}
24
25#[derive(Clone)]
27pub struct AuthorizationLayer {
28 service: Arc<AuthorizationService>,
29}
30
31impl AuthorizationLayer {
32 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
52pub 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 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 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 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 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 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
163fn 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
175fn to_resource_type(path: &str) -> Resource {
177 let parts: Vec<&str> = path
179 .trim_end_matches('/')
180 .split('/')
181 .collect();
182
183 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
194fn 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}