use crate::error::OpsError;
use crate::state::OpsState;
use axum::{body::Body, extract::State, http::Request, middleware::Next, response::Response};
use klieo_auth_common::Headers;
use std::sync::Arc;
pub(crate) const OPS_READ_SCOPE: &str = "klieo.ops.read";
pub(crate) struct HttpHeadersAdapter<'a>(pub &'a axum::http::HeaderMap);
impl<'a> Headers for HttpHeadersAdapter<'a> {
fn get(&self, name: &str) -> Option<&str> {
self.0.get(name).and_then(|v| v.to_str().ok())
}
}
pub async fn require_auth(
State(state): State<Arc<OpsState>>,
mut request: Request<Body>,
next: Next,
) -> Result<Response, OpsError> {
let adapter = HttpHeadersAdapter(request.headers());
let identity = state
.authenticator
.authenticate(&adapter, &[])
.await
.map_err(|e| {
tracing::warn!(reason = %e, "ops request rejected: auth failure");
OpsError::Unauthorized
})?;
if !identity.has_scope(OPS_READ_SCOPE) {
tracing::warn!(
principal = %identity.as_str(),
"ops request rejected: missing scope klieo.ops.read"
);
return Err(OpsError::Forbidden);
}
request.extensions_mut().insert(identity);
Ok(next.run(request).await)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderMap;
#[test]
fn http_headers_adapter_case_insensitive() {
let mut map = HeaderMap::new();
map.insert("Authorization", "Bearer tok".parse().unwrap());
let adapter = HttpHeadersAdapter(&map);
assert_eq!(adapter.get("authorization"), Some("Bearer tok"));
assert_eq!(adapter.get("AUTHORIZATION"), Some("Bearer tok"));
assert!(adapter.get("x-missing").is_none());
}
}