klieo-ops-api 3.3.0

Read-only HTTP monitoring router for klieo agentic systems.
Documentation
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());
    }
}