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>,
22}
23
24#[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
50pub 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 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 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 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 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 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
161fn 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
173fn to_resource_type(path: &str) -> Resource {
175 let parts: Vec<&str> = path
177 .trim_end_matches('/')
178 .split('/')
179 .collect();
180
181 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
192fn 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}