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(&'static str),
42 #[cfg(feature = "rate-limit")]
44 RateLimited,
45}
46
47pub type AuthError = AuthRejection;
49
50impl IntoResponse for AuthRejection {
51 fn into_response(self) -> axum::response::Response {
52 match self {
53 AuthRejection::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg).into_response(),
54 #[cfg(feature = "rate-limit")]
55 AuthRejection::RateLimited => {
56 (StatusCode::TOO_MANY_REQUESTS, "rate limit exceeded").into_response()
57 }
58 }
59 }
60}
61
62impl<S> FromRequestParts<S> for AuthPrincipal
63where
64 S: Send + Sync,
65{
66 type Rejection = AuthRejection;
67
68 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
69 let Extension(auth): Extension<Arc<IjimaAuth>> =
70 Extension::from_request_parts(parts, _state)
71 .await
72 .map_err(|_| AuthRejection::Unauthorized("auth state not installed"))?;
73
74 let header = parts
75 .headers
76 .get(axum::http::header::AUTHORIZATION)
77 .and_then(|h| h.to_str().ok())
78 .ok_or(AuthRejection::Unauthorized("missing Authorization header"))?;
79
80 let token = header
81 .strip_prefix("Bearer ")
82 .ok_or(AuthRejection::Unauthorized("expected 'Bearer <token>'"))?;
83
84 let principal = auth
85 .verify_bearer(token)
86 .map_err(|_| AuthRejection::Unauthorized("invalid capability token"))?;
87
88 #[cfg(feature = "rate-limit")]
92 {
93 if let Some(rl) = parts.extensions.get::<crate::rate_limit::RateLimitState>() {
94 crate::rate_limit::consume(rl, &principal)
95 .map_err(|_| AuthRejection::RateLimited)?;
96 }
97 }
98
99 Ok(AuthPrincipal(principal))
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use ijima_core::capabilities::MEMORY_READ;
107
108 fn build_request(bearer: Option<String>) -> (Parts, ()) {
109 let mut builder = axum::http::Request::<()>::builder();
110 if let Some(b) = bearer {
111 builder = builder.header("authorization", format!("Bearer {b}"));
112 }
113 let req = builder.body(()).unwrap();
114 req.into_parts()
115 }
116
117 #[tokio::test]
118 async fn valid_bearer_header_yields_principal() {
119 let auth = Arc::new(IjimaAuth::from_embedded_policy().expect("policy must load"));
120 let bearer = auth.issue_bearer("elliott", MEMORY_READ).expect("issue");
121
122 let (mut parts, ()) = build_request(Some(bearer));
123 parts.extensions.insert(auth.clone());
124
125 let state: () = ();
126 let got = AuthPrincipal::from_request_parts(&mut parts, &state)
127 .await
128 .expect("must extract");
129 assert_eq!(got.0.principal.as_str(), "elliott");
130 assert_eq!(got.0.capability, MEMORY_READ);
131 }
132
133 #[tokio::test]
134 async fn missing_header_is_401() {
135 let auth = Arc::new(IjimaAuth::from_embedded_policy().expect("policy must load"));
136 let (mut parts, ()) = build_request(None);
137 parts.extensions.insert(auth);
138
139 let state: () = ();
140 let err = AuthPrincipal::from_request_parts(&mut parts, &state)
141 .await
142 .expect_err("must reject");
143 assert_eq!(err.into_response().status(), StatusCode::UNAUTHORIZED);
144 }
145}