1use std::sync::Arc;
24use std::time::SystemTime;
25
26use axum::body::Body;
27use axum::extract::{Request, State};
28use axum::http::header::{AUTHORIZATION, CONTENT_LENGTH};
29use axum::http::HeaderValue;
30use axum::middleware::Next;
31use axum::response::{IntoResponse, Response};
32use axum::Router;
33
34use crate::error::ApiError;
35use crate::keys::{parse_bearer, KeyStore};
36use crate::metering::Metering;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct KeyId(pub String);
44
45impl KeyId {
46 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl std::fmt::Display for KeyId {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58pub const MAX_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
66
67#[derive(Debug, Clone)]
69pub struct AuthMetering {
70 keys: Arc<KeyStore>,
71 metering: Arc<Metering>,
72}
73
74impl AuthMetering {
75 pub fn new(keys: Arc<KeyStore>, metering: Arc<Metering>) -> Self {
77 AuthMetering { keys, metering }
78 }
79}
80
81pub fn metered<S>(router: Router<S>, auth: Arc<AuthMetering>) -> Router<S>
86where
87 S: Clone + Send + Sync + 'static,
88{
89 router.route_layer(axum::middleware::from_fn_with_state(auth, auth_and_meter))
90}
91
92async fn auth_and_meter(
94 State(auth): State<Arc<AuthMetering>>,
95 mut request: Request,
96 next: Next,
97) -> Response {
98 let started = SystemTime::now();
103 let method = request.method().as_str().to_string();
104 let path = request.uri().path().to_string();
105
106 let presented = parse_bearer(
107 request
108 .headers()
109 .get(AUTHORIZATION)
110 .and_then(|value| value.to_str().ok()),
111 );
112 let Some((key_id, secret)) = presented else {
113 return unauthorized();
114 };
115 let key_id = match auth.keys.verify(key_id, secret) {
116 Ok(key_id) => key_id,
117 Err(_) => return unauthorized(),
118 };
119 request.extensions_mut().insert(KeyId(key_id.clone()));
120
121 let response = next.run(request).await;
122 measure_and_meter(
123 auth.metering.as_ref(),
124 &key_id,
125 &method,
126 path,
127 response,
128 started,
129 )
130 .await
131}
132
133fn unauthorized() -> Response {
140 let mut response = ApiError::Unauthorized.into_response();
141 response.headers_mut().insert(
142 axum::http::header::WWW_AUTHENTICATE,
143 HeaderValue::from_static("Bearer realm=\"codetopo\""),
144 );
145 response
146}
147
148async fn measure_and_meter(
150 metering: &Metering,
151 key_id: &str,
152 method: &str,
153 path: String,
154 response: Response,
155 started: SystemTime,
156) -> Response {
157 let status = response.status().as_u16();
158 let (parts, body) = response.into_parts();
159
160 let (body, response_bytes) = match declared_length(&parts) {
161 Some(len) if len > MAX_RESPONSE_BYTES => (body, len as u64),
164 _ => match axum::body::to_bytes(body, MAX_RESPONSE_BYTES).await {
165 Ok(bytes) => {
166 let len = bytes.len() as u64;
167 (Body::from(bytes), len)
168 }
169 Err(err) => {
174 eprintln!("warning: response body exceeded the metering buffer cap: {err}");
175 return ApiError::Internal(format!(
176 "response exceeded the {MAX_RESPONSE_BYTES}-byte metering buffer limit"
177 ))
178 .into_response();
179 }
180 },
181 };
182
183 let record = metering.build(key_id, method, &path, status, response_bytes, started);
184 metering.append(&record);
188
189 Response::from_parts(parts, body)
190}
191
192fn declared_length(parts: &axum::http::response::Parts) -> Option<usize> {
194 parts
195 .headers
196 .get(CONTENT_LENGTH)
197 .and_then(|value| value.to_str().ok())
198 .and_then(|value| value.parse().ok())
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use axum::http::HeaderMap;
205
206 fn auth(key: &str) -> (Arc<AuthMetering>, tempfile::TempDir) {
207 let dir = tempfile::tempdir().expect("temp dir");
208 let keys = KeyStore::parse(&format!("{key}:s3cr3t\n"));
209 let metering = Arc::new(Metering::new(&dir.path().join("metering.log")));
210 (Arc::new(AuthMetering::new(Arc::new(keys), metering)), dir)
211 }
212
213 fn bearer(value: &str) -> HeaderMap {
214 let mut headers = HeaderMap::new();
215 headers.insert(AUTHORIZATION, value.parse().expect("header value"));
216 headers
217 }
218
219 fn resolve(auth: &AuthMetering, headers: &HeaderMap) -> Result<String, ApiError> {
221 let Some((key_id, secret)) = parse_bearer(
222 headers
223 .get(AUTHORIZATION)
224 .and_then(|value| value.to_str().ok()),
225 ) else {
226 return Err(ApiError::Unauthorized);
227 };
228 auth.keys.verify(key_id, secret)
229 }
230
231 #[test]
232 fn key_id_exposes_identifier_not_secret() {
233 let key_id = KeyId("research-agent".to_string());
234 assert_eq!(key_id.as_str(), "research-agent");
235 assert_eq!(key_id.to_string(), "research-agent");
236 }
237
238 #[test]
239 fn correct_credential_authenticates() {
240 let (auth, _dir) = auth("agent");
241 assert_eq!(
242 resolve(&auth, &bearer("Bearer agent:s3cr3t")).unwrap(),
243 "agent"
244 );
245 }
246
247 #[test]
248 fn scheme_is_case_insensitive() {
249 let (auth, _dir) = auth("agent");
250 assert_eq!(
251 resolve(&auth, &bearer("bearer agent:s3cr3t")).unwrap(),
252 "agent"
253 );
254 }
255
256 #[test]
257 fn wrong_secret_is_unauthorized() {
258 let (auth, _dir) = auth("agent");
259 assert!(matches!(
260 resolve(&auth, &bearer("Bearer agent:wrong")),
261 Err(ApiError::Unauthorized)
262 ));
263 }
264
265 #[test]
266 fn unknown_key_is_unauthorized() {
267 let (auth, _dir) = auth("agent");
268 assert!(matches!(
269 resolve(&auth, &bearer("Bearer nobody:s3cr3t")),
270 Err(ApiError::Unauthorized)
271 ));
272 }
273
274 #[test]
275 fn missing_header_is_unauthorized() {
276 let (auth, _dir) = auth("agent");
277 assert!(matches!(
278 resolve(&auth, &HeaderMap::new()),
279 Err(ApiError::Unauthorized)
280 ));
281 }
282
283 #[test]
284 fn wrong_scheme_is_unauthorized() {
285 let (auth, _dir) = auth("agent");
286 assert!(matches!(
287 resolve(&auth, &bearer("Basic agent:s3cr3t")),
288 Err(ApiError::Unauthorized)
289 ));
290 }
291
292 #[test]
293 fn token_without_colon_is_unauthorized() {
294 let (auth, _dir) = auth("agent");
295 assert!(matches!(
296 resolve(&auth, &bearer("Bearer agentonly")),
297 Err(ApiError::Unauthorized)
298 ));
299 }
300
301 #[test]
302 fn declared_length_reads_content_length() {
303 let response = Response::builder()
304 .header(CONTENT_LENGTH, "42")
305 .body(Body::empty())
306 .unwrap();
307 let (parts, _body) = response.into_parts();
308 assert_eq!(declared_length(&parts), Some(42));
309 }
310
311 #[test]
312 fn declared_length_absent_when_undeclared() {
313 let (parts, _body) = Response::new(Body::empty()).into_parts();
314 assert_eq!(declared_length(&parts), None);
315 }
316
317 #[test]
318 fn metering_records_the_served_body() {
319 let (auth, dir) = auth("agent");
320 let response = Response::builder()
321 .status(200)
322 .header(CONTENT_LENGTH, "5")
323 .body(Body::from("hello"))
324 .unwrap();
325 let out = tokio::runtime::Builder::new_current_thread()
326 .enable_all()
327 .build()
328 .unwrap()
329 .block_on(measure_and_meter(
330 auth.metering.as_ref(),
331 "agent",
332 "GET",
333 "/v1/graphs/x/stats".to_string(),
334 response,
335 SystemTime::now(),
336 ));
337 assert_eq!(out.status(), 200);
338 let log = std::fs::read_to_string(dir.path().join("metering.log")).unwrap();
339 let record: serde_json::Value = serde_json::from_str(log.trim()).unwrap();
340 assert_eq!(record["key_id"], "agent");
341 assert_eq!(record["method"], "GET");
342 assert_eq!(record["path"], "/v1/graphs/x/stats");
343 assert_eq!(record["status"], 200);
344 assert_eq!(record["response_bytes"], 5);
345 }
346}