1use std::sync::Arc;
29use std::time::SystemTime;
30
31use axum::body::Body;
32use axum::extract::{Request, State};
33use axum::http::header::{AUTHORIZATION, CONTENT_LENGTH};
34use axum::http::HeaderValue;
35use axum::middleware::Next;
36use axum::response::{IntoResponse, Response};
37use axum::Router;
38
39use crate::error::ApiError;
40use crate::keys::{parse_bearer, KeyStore};
41use crate::metering::Metering;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct KeyId(pub String);
49
50impl KeyId {
51 pub fn as_str(&self) -> &str {
53 &self.0
54 }
55}
56
57impl std::fmt::Display for KeyId {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.write_str(&self.0)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct KeyPlan(pub String);
74
75impl KeyPlan {
76 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80
81 pub fn is_self_hosted(&self) -> bool {
83 self.0 == crate::keys::SELF_HOSTED_PLAN
84 }
85}
86
87pub const MAX_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
95
96#[derive(Debug, Clone)]
98pub struct AuthMetering {
99 keys: Arc<KeyStore>,
100 metering: Arc<Metering>,
101}
102
103impl AuthMetering {
104 pub fn new(keys: Arc<KeyStore>, metering: Arc<Metering>) -> Self {
106 AuthMetering { keys, metering }
107 }
108}
109
110pub fn metered<S>(router: Router<S>, auth: Arc<AuthMetering>) -> Router<S>
115where
116 S: Clone + Send + Sync + 'static,
117{
118 router.route_layer(axum::middleware::from_fn_with_state(auth, auth_and_meter))
119}
120
121async fn auth_and_meter(
123 State(auth): State<Arc<AuthMetering>>,
124 mut request: Request,
125 next: Next,
126) -> Response {
127 let started = SystemTime::now();
132 let method = request.method().as_str().to_string();
133 let path = request.uri().path().to_string();
134
135 let presented = parse_bearer(
136 request
137 .headers()
138 .get(AUTHORIZATION)
139 .and_then(|value| value.to_str().ok()),
140 );
141 let Some((key_id, secret)) = presented else {
142 return unauthorized();
143 };
144 let verified = match auth.keys.verify(key_id, secret) {
145 Ok(verified) => verified,
146 Err(_) => return unauthorized(),
147 };
148 request
149 .extensions_mut()
150 .insert(KeyId(verified.key_id.clone()));
151 request
152 .extensions_mut()
153 .insert(KeyPlan(verified.plan.clone()));
154
155 let limits = crate::limits::plan_limits(&verified.plan);
165 if let Some(max_queries) = limits.max_queries_per_month {
166 let is_index_op = method.as_str() == "POST" && path.as_str() == "/v1/index";
167 if !is_index_op {
168 let month = crate::metering::current_month();
169 if auth
170 .metering
171 .query_count_reaches(&verified.key_id, &month, max_queries)
172 {
173 let used = auth.metering.usage_in_month(&verified.key_id, &month).queries;
174 return ApiError::query_quota(&verified.plan, max_queries, used, &month)
175 .into_response();
176 }
177 }
178 }
179
180 let response = next.run(request).await;
181 measure_and_meter(
182 auth.metering.as_ref(),
183 &verified.key_id,
184 &verified.plan,
185 &method,
186 path,
187 response,
188 started,
189 )
190 .await
191}
192
193fn unauthorized() -> Response {
200 let mut response = ApiError::Unauthorized.into_response();
201 response.headers_mut().insert(
202 axum::http::header::WWW_AUTHENTICATE,
203 HeaderValue::from_static("Bearer realm=\"codetopo\""),
204 );
205 response
206}
207
208async fn measure_and_meter(
210 metering: &Metering,
211 key_id: &str,
212 plan: &str,
213 method: &str,
214 path: String,
215 response: Response,
216 started: SystemTime,
217) -> Response {
218 let status = response.status().as_u16();
219 let (parts, body) = response.into_parts();
220
221 let (body, response_bytes) = match declared_length(&parts) {
222 Some(len) if len > MAX_RESPONSE_BYTES => (body, len as u64),
225 _ => match axum::body::to_bytes(body, MAX_RESPONSE_BYTES).await {
226 Ok(bytes) => {
227 let len = bytes.len() as u64;
228 (Body::from(bytes), len)
229 }
230 Err(err) => {
235 eprintln!("warning: response body exceeded the metering buffer cap: {err}");
236 return ApiError::Internal(format!(
237 "response exceeded the {MAX_RESPONSE_BYTES}-byte metering buffer limit"
238 ))
239 .into_response();
240 }
241 },
242 };
243
244 let record = metering.build(key_id, plan, method, &path, status, response_bytes, started);
245 metering.append(&record);
249
250 Response::from_parts(parts, body)
251}
252
253fn declared_length(parts: &axum::http::response::Parts) -> Option<usize> {
255 parts
256 .headers
257 .get(CONTENT_LENGTH)
258 .and_then(|value| value.to_str().ok())
259 .and_then(|value| value.parse().ok())
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use axum::http::HeaderMap;
266
267 fn auth(key: &str) -> (Arc<AuthMetering>, tempfile::TempDir) {
268 let dir = tempfile::tempdir().expect("temp dir");
269 let keys = KeyStore::parse(&format!("{key}:s3cr3t\n"));
270 let metering = Arc::new(Metering::new(&dir.path().join("metering.log")));
271 (Arc::new(AuthMetering::new(Arc::new(keys), metering)), dir)
272 }
273
274 fn bearer(value: &str) -> HeaderMap {
275 let mut headers = HeaderMap::new();
276 headers.insert(AUTHORIZATION, value.parse().expect("header value"));
277 headers
278 }
279
280 fn resolve(auth: &AuthMetering, headers: &HeaderMap) -> Result<String, ApiError> {
282 let Some((key_id, secret)) = parse_bearer(
283 headers
284 .get(AUTHORIZATION)
285 .and_then(|value| value.to_str().ok()),
286 ) else {
287 return Err(ApiError::Unauthorized);
288 };
289 auth.keys.verify(key_id, secret).map(|verified| verified.key_id)
290 }
291
292 #[test]
293 fn key_id_exposes_identifier_not_secret() {
294 let key_id = KeyId("research-agent".to_string());
295 assert_eq!(key_id.as_str(), "research-agent");
296 assert_eq!(key_id.to_string(), "research-agent");
297 }
298
299 #[test]
300 fn key_plan_names_the_self_hosted_plan() {
301 assert!(KeyPlan(crate::keys::SELF_HOSTED_PLAN.to_string()).is_self_hosted());
302 let hosted = KeyPlan("hosted".to_string());
303 assert!(!hosted.is_self_hosted());
304 assert_eq!(hosted.as_str(), "hosted");
305 }
306
307 #[test]
308 fn correct_credential_authenticates() {
309 let (auth, _dir) = auth("agent");
310 assert_eq!(
311 resolve(&auth, &bearer("Bearer agent:s3cr3t")).unwrap(),
312 "agent"
313 );
314 }
315
316 #[test]
317 fn scheme_is_case_insensitive() {
318 let (auth, _dir) = auth("agent");
319 assert_eq!(
320 resolve(&auth, &bearer("bearer agent:s3cr3t")).unwrap(),
321 "agent"
322 );
323 }
324
325 #[test]
326 fn wrong_secret_is_unauthorized() {
327 let (auth, _dir) = auth("agent");
328 assert!(matches!(
329 resolve(&auth, &bearer("Bearer agent:wrong")),
330 Err(ApiError::Unauthorized)
331 ));
332 }
333
334 #[test]
335 fn unknown_key_is_unauthorized() {
336 let (auth, _dir) = auth("agent");
337 assert!(matches!(
338 resolve(&auth, &bearer("Bearer nobody:s3cr3t")),
339 Err(ApiError::Unauthorized)
340 ));
341 }
342
343 #[test]
344 fn missing_header_is_unauthorized() {
345 let (auth, _dir) = auth("agent");
346 assert!(matches!(
347 resolve(&auth, &HeaderMap::new()),
348 Err(ApiError::Unauthorized)
349 ));
350 }
351
352 #[test]
353 fn wrong_scheme_is_unauthorized() {
354 let (auth, _dir) = auth("agent");
355 assert!(matches!(
356 resolve(&auth, &bearer("Basic agent:s3cr3t")),
357 Err(ApiError::Unauthorized)
358 ));
359 }
360
361 #[test]
362 fn token_without_colon_is_unauthorized() {
363 let (auth, _dir) = auth("agent");
364 assert!(matches!(
365 resolve(&auth, &bearer("Bearer agentonly")),
366 Err(ApiError::Unauthorized)
367 ));
368 }
369
370 #[test]
371 fn declared_length_reads_content_length() {
372 let response = Response::builder()
373 .header(CONTENT_LENGTH, "42")
374 .body(Body::empty())
375 .unwrap();
376 let (parts, _body) = response.into_parts();
377 assert_eq!(declared_length(&parts), Some(42));
378 }
379
380 #[test]
381 fn declared_length_absent_when_undeclared() {
382 let (parts, _body) = Response::new(Body::empty()).into_parts();
383 assert_eq!(declared_length(&parts), None);
384 }
385
386 #[test]
387 fn metering_records_the_served_body() {
388 let (auth, dir) = auth("agent");
389 let response = Response::builder()
390 .status(200)
391 .header(CONTENT_LENGTH, "5")
392 .body(Body::from("hello"))
393 .unwrap();
394 let out = tokio::runtime::Builder::new_current_thread()
395 .enable_all()
396 .build()
397 .unwrap()
398 .block_on(measure_and_meter(
399 auth.metering.as_ref(),
400 "agent",
401 "self-hosted",
402 "GET",
403 "/v1/graphs/x/stats".to_string(),
404 response,
405 SystemTime::now(),
406 ));
407 assert_eq!(out.status(), 200);
408 let log = std::fs::read_to_string(dir.path().join("metering.log")).unwrap();
409 let record: serde_json::Value = serde_json::from_str(log.trim()).unwrap();
410 assert_eq!(record["key_id"], "agent");
411 assert_eq!(record["plan"], "self-hosted");
412 assert_eq!(record["method"], "GET");
413 assert_eq!(record["path"], "/v1/graphs/x/stats");
414 assert_eq!(record["status"], 200);
415 assert_eq!(record["response_bytes"], 5);
416 }
417}