ijima_server/
extractor.rs1use axum::{
15 Extension,
16 extract::FromRequestParts,
17 http::{StatusCode, request::Parts},
18 response::IntoResponse,
19};
20use std::sync::Arc;
21
22use crate::auth::{AuthenticatedPrincipal, IjimaAuth};
23
24#[derive(Debug, Clone)]
33pub struct AuthPrincipal(pub AuthenticatedPrincipal);
34
35#[derive(Debug)]
39pub enum AuthRejection {
40 Unauthorized(String),
44 #[cfg(feature = "rate-limit")]
46 RateLimited,
47}
48
49pub type AuthError = AuthRejection;
51
52impl IntoResponse for AuthRejection {
53 fn into_response(self) -> axum::response::Response {
54 match self {
55 AuthRejection::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg).into_response(),
56 #[cfg(feature = "rate-limit")]
57 AuthRejection::RateLimited => {
58 (StatusCode::TOO_MANY_REQUESTS, "rate limit exceeded").into_response()
59 }
60 }
61 }
62}
63
64impl<S> FromRequestParts<S> for AuthPrincipal
65where
66 S: Send + Sync,
67{
68 type Rejection = AuthRejection;
69
70 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
71 let Extension(auth): Extension<Arc<IjimaAuth>> =
72 Extension::from_request_parts(parts, _state)
73 .await
74 .map_err(|_| AuthRejection::Unauthorized("auth state not installed".into()))?;
75
76 let header = parts
77 .headers
78 .get(axum::http::header::AUTHORIZATION)
79 .and_then(|h| h.to_str().ok())
80 .ok_or(AuthRejection::Unauthorized(
81 "missing Authorization header".into(),
82 ))?;
83
84 let token = header
85 .strip_prefix("Bearer ")
86 .ok_or(AuthRejection::Unauthorized(
87 "expected 'Bearer <token>'".into(),
88 ))?;
89
90 let principal = auth.verify_bearer(token).map_err(|e| {
91 AuthRejection::Unauthorized(e.to_string())
95 })?;
96
97 #[cfg(feature = "rate-limit")]
101 {
102 if let Some(rl) = parts.extensions.get::<crate::rate_limit::RateLimitState>() {
103 crate::rate_limit::consume(rl, &principal)
104 .map_err(|_| AuthRejection::RateLimited)?;
105 }
106 }
107
108 Ok(AuthPrincipal(principal))
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use ijima_core::capabilities::MEMORY_READ;
116
117 fn build_request(bearer: Option<String>) -> (Parts, ()) {
118 let mut builder = axum::http::Request::<()>::builder();
119 if let Some(b) = bearer {
120 builder = builder.header("authorization", format!("Bearer {b}"));
121 }
122 let req = builder.body(()).unwrap();
123 req.into_parts()
124 }
125
126 #[tokio::test]
127 async fn valid_bearer_header_yields_principal() {
128 let auth = Arc::new(IjimaAuth::from_embedded_policy().expect("policy must load"));
129 let bearer = auth.issue_bearer("elliott", MEMORY_READ).expect("issue");
130
131 let (mut parts, ()) = build_request(Some(bearer));
132 parts.extensions.insert(auth.clone());
133
134 let state: () = ();
135 let got = AuthPrincipal::from_request_parts(&mut parts, &state)
136 .await
137 .expect("must extract");
138 assert_eq!(got.0.principal.as_str(), "elliott");
139 assert!(got.0.may(MEMORY_READ));
140 }
141
142 #[tokio::test]
143 async fn missing_header_is_401() {
144 let auth = Arc::new(IjimaAuth::from_embedded_policy().expect("policy must load"));
145 let (mut parts, ()) = build_request(None);
146 parts.extensions.insert(auth);
147
148 let state: () = ();
149 let err = AuthPrincipal::from_request_parts(&mut parts, &state)
150 .await
151 .expect_err("must reject");
152 assert_eq!(err.into_response().status(), StatusCode::UNAUTHORIZED);
153 }
154}