use axum::{body::Body, extract::Request, http::Method, middleware::Next, response::Response};
use serde::Serialize;
use tracing::{info, warn};
use uuid::Uuid;
use super::auth::Principal;
#[derive(Debug, Serialize)]
pub struct AuditEvent {
pub request_id: String,
pub principal_id: String,
pub scopes: Vec<String>,
pub method: String,
pub path: String,
pub result: AuditResult,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AuditResult {
Success,
Denied,
Error,
}
impl AuditEvent {
pub fn new(
request_id: impl Into<String>,
principal: &Principal,
method: &Method,
path: impl Into<String>,
result: AuditResult,
) -> Self {
Self {
request_id: request_id.into(),
principal_id: principal.id.clone(),
scopes: principal.scopes.iter().map(|s| s.to_string()).collect(),
method: method.to_string(),
path: path.into(),
result,
}
}
pub fn log(&self) {
match self.result {
AuditResult::Success => {
info!(
target: "audit",
request_id = %self.request_id,
principal = %self.principal_id,
method = %self.method,
path = %self.path,
result = "success",
"audit: operation completed"
);
}
AuditResult::Denied => {
warn!(
target: "audit",
request_id = %self.request_id,
principal = %self.principal_id,
method = %self.method,
path = %self.path,
result = "denied",
"audit: operation denied"
);
}
AuditResult::Error => {
warn!(
target: "audit",
request_id = %self.request_id,
principal = %self.principal_id,
method = %self.method,
path = %self.path,
result = "error",
"audit: operation failed"
);
}
}
}
}
pub async fn audit_middleware(request: Request<Body>, next: Next) -> Response {
let method = request.method().clone();
let path = request.uri().path().to_string();
let request_id = Uuid::new_v4().to_string();
let is_mutating = matches!(
method,
Method::POST | Method::PUT | Method::DELETE | Method::PATCH
);
if !is_mutating {
return next.run(request).await;
}
let principal = request
.extensions()
.get::<Principal>()
.cloned()
.unwrap_or_else(|| Principal {
id: "anonymous".to_string(),
scopes: std::collections::HashSet::new(),
});
let response = next.run(request).await;
let result = if response.status().is_success() {
AuditResult::Success
} else if response.status().as_u16() == 401 || response.status().as_u16() == 403 {
AuditResult::Denied
} else {
AuditResult::Error
};
let event = AuditEvent::new(request_id, &principal, &method, path, result);
event.log();
response
}