use axum::{
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use constant_time_eq::constant_time_eq;
use serde_json::json;
use crate::store::api_keys::{hash_key, Permission};
use crate::AppState;
#[derive(Debug, Clone)]
pub struct ResolvedPermissions {
pub permissions: Vec<Permission>,
pub prefix: Option<String>,
pub is_admin: bool,
}
impl ResolvedPermissions {
pub fn full_admin() -> Self {
Self {
permissions: vec![],
prefix: None,
is_admin: true,
}
}
pub fn can_read(&self) -> bool {
self.is_admin
|| self
.permissions
.iter()
.any(|p| matches!(p, Permission::Read | Permission::Admin))
}
pub fn can_write(&self) -> bool {
self.is_admin
|| self
.permissions
.iter()
.any(|p| matches!(p, Permission::Write | Permission::Admin))
}
pub fn can_delete(&self) -> bool {
self.is_admin
|| self
.permissions
.iter()
.any(|p| matches!(p, Permission::Delete | Permission::Admin))
}
pub fn can_admin(&self) -> bool {
self.is_admin
|| self
.permissions
.iter()
.any(|p| matches!(p, Permission::Admin))
}
pub fn matches_prefix(&self, key: &str) -> bool {
self.is_admin
|| match &self.prefix {
None => true,
Some(p) => key.starts_with(p.as_str()),
}
}
}
pub async fn require_api_key(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Response {
let has_admin_key = state.api_key.is_some();
let has_scoped_keys = state.store.has_api_keys().unwrap_or(false);
if !has_admin_key && !has_scoped_keys {
request
.extensions_mut()
.insert(ResolvedPermissions::full_admin());
return next.run(request).await;
}
let token = request
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
let Some(token) = token else {
return unauthorized();
};
if let Some(ref expected) = state.api_key {
if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
request
.extensions_mut()
.insert(ResolvedPermissions::full_admin());
return next.run(request).await;
}
}
let token_hash = hash_key(token);
match state.store.find_api_key_by_hash(&token_hash) {
Ok(Some(record)) => {
let perms = ResolvedPermissions {
permissions: record.permissions,
prefix: record.prefix,
is_admin: false,
};
request.extensions_mut().insert(perms);
next.run(request).await
}
_ => unauthorized(),
}
}
fn unauthorized() -> Response {
(
StatusCode::UNAUTHORIZED,
Json(json!({"error": "unauthorized — valid API key required for this endpoint"})),
)
.into_response()
}