use actix_web::{dev::ServiceRequest, web, HttpMessage, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
pub type Database = std::sync::Arc<dyn DatabaseOps + Send + Sync>;
#[allow(dead_code)]
#[async_trait::async_trait]
pub trait DatabaseOps {
async fn create(&self, table: &str, data: serde_json::Value) -> Result<serde_json::Value, String>;
async fn get_by_id(&self, table: &str, id: &str) -> Result<Option<serde_json::Value>, String>;
async fn query(&self, table: &str, filter: serde_json::Value) -> Result<Vec<serde_json::Value>, String>;
async fn count(&self, table: &str, filter: serde_json::Value) -> Result<i64, String>;
async fn update(&self, table: &str, id: &str, data: serde_json::Value) -> Result<(), String>;
async fn delete(&self, table: &str, id: &str) -> Result<(), String>;
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuditCategory {
Authentication,
Authorization,
UserManagement,
SessionManagement,
WorkspaceManagement,
TeamManagement,
Configuration,
DataTransfer,
Administration,
System,
ApiAccess,
Sso,
}
impl AuditCategory {
pub fn as_str(&self) -> &'static str {
match self {
Self::Authentication => "authentication",
Self::Authorization => "authorization",
Self::UserManagement => "user_management",
Self::SessionManagement => "session_management",
Self::WorkspaceManagement => "workspace_management",
Self::TeamManagement => "team_management",
Self::Configuration => "configuration",
Self::DataTransfer => "data_transfer",
Self::Administration => "administration",
Self::System => "system",
Self::ApiAccess => "api_access",
Self::Sso => "sso",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuditAction {
Login,
LoginFailed,
Logout,
TokenRefresh,
PasswordChange,
PasswordReset,
MfaEnabled,
MfaDisabled,
AccessGranted,
AccessDenied,
PermissionChanged,
UserCreated,
UserUpdated,
UserDeleted,
UserSuspended,
UserActivated,
UserInvited,
InviteAccepted,
SubscriptionChanged,
SessionHarvested,
SessionViewed,
SessionExported,
SessionDeleted,
SessionArchived,
SessionShared,
SessionUnshared,
BulkExport,
BulkDelete,
WorkspaceCreated,
WorkspaceUpdated,
WorkspaceDeleted,
WorkspaceLinked,
WorkspaceUnlinked,
TeamCreated,
TeamUpdated,
TeamDeleted,
MemberAdded,
MemberRemoved,
RoleChanged,
SettingsUpdated,
ProviderConfigured,
ProviderDisabled,
IdpConfigured,
IdpUpdated,
IdpDeleted,
DataExported,
DataImported,
BackupCreated,
BackupRestored,
AdminActionPerformed,
SystemConfigChanged,
RetentionPolicyApplied,
DataPurged,
ServiceStarted,
ServiceStopped,
ErrorOccurred,
MaintenanceStarted,
MaintenanceCompleted,
SsoLoginInitiated,
SsoLoginCompleted,
SsoLoginFailed,
SloInitiated,
SloCompleted,
Created,
Updated,
Deleted,
Viewed,
Listed,
Searched,
}
impl AuditAction {
pub fn as_str(&self) -> &str {
let name = format!("{:?}", self);
&name.to_lowercase()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum AuditOutcome {
#[default]
Success,
Failure,
Partial,
Denied,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub category: AuditCategory,
pub action: AuditAction,
pub outcome: AuditOutcome,
pub actor: Option<AuditActor>,
pub resource: Option<AuditResource>,
pub request: Option<AuditRequest>,
pub details: HashMap<String, serde_json::Value>,
pub description: String,
pub error: Option<String>,
pub organization_id: Option<String>,
pub correlation_id: Option<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditActor {
pub user_id: String,
pub email: String,
pub display_name: Option<String>,
pub tier: Option<String>,
pub auth_method: Option<String>,
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditResource {
pub resource_type: String,
pub resource_id: String,
pub name: Option<String>,
pub parent: Option<Box<AuditResource>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditRequest {
pub method: String,
pub path: String,
pub query: Option<String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub request_id: String,
pub status_code: Option<u16>,
pub duration_ms: Option<u64>,
}
#[derive(Default)]
pub struct AuditEventBuilder {
category: Option<AuditCategory>,
action: Option<AuditAction>,
outcome: AuditOutcome,
actor: Option<AuditActor>,
resource: Option<AuditResource>,
request: Option<AuditRequest>,
details: HashMap<String, serde_json::Value>,
description: Option<String>,
error: Option<String>,
organization_id: Option<String>,
correlation_id: Option<String>,
tags: Vec<String>,
}
impl AuditEventBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn category(mut self, category: AuditCategory) -> Self {
self.category = Some(category);
self
}
pub fn action(mut self, action: AuditAction) -> Self {
self.action = Some(action);
self
}
pub fn outcome(mut self, outcome: AuditOutcome) -> Self {
self.outcome = outcome;
self
}
pub fn success(mut self) -> Self {
self.outcome = AuditOutcome::Success;
self
}
pub fn failure(mut self, error: impl Into<String>) -> Self {
self.outcome = AuditOutcome::Failure;
self.error = Some(error.into());
self
}
pub fn denied(mut self) -> Self {
self.outcome = AuditOutcome::Denied;
self
}
pub fn actor(mut self, actor: AuditActor) -> Self {
self.actor = Some(actor);
self
}
pub fn actor_from_user(mut self, user_id: &str, email: &str) -> Self {
self.actor = Some(AuditActor {
user_id: user_id.to_string(),
email: email.to_string(),
display_name: None,
tier: None,
auth_method: None,
session_id: None,
});
self
}
pub fn resource(mut self, resource_type: &str, resource_id: &str) -> Self {
self.resource = Some(AuditResource {
resource_type: resource_type.to_string(),
resource_id: resource_id.to_string(),
name: None,
parent: None,
});
self
}
pub fn resource_with_name(
mut self,
resource_type: &str,
resource_id: &str,
name: &str,
) -> Self {
self.resource = Some(AuditResource {
resource_type: resource_type.to_string(),
resource_id: resource_id.to_string(),
name: Some(name.to_string()),
parent: None,
});
self
}
pub fn request(mut self, request: AuditRequest) -> Self {
self.request = Some(request);
self
}
pub fn request_from_http(mut self, req: &HttpRequest) -> Self {
let connection_info = req.connection_info();
self.request = Some(AuditRequest {
method: req.method().to_string(),
path: req.path().to_string(),
query: req
.query_string()
.is_empty()
.then(|| None)
.unwrap_or(Some(req.query_string().to_string())),
ip_address: connection_info.realip_remote_addr().map(|s| s.to_string()),
user_agent: req
.headers()
.get("user-agent")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string()),
request_id: Uuid::new_v4().to_string(),
status_code: None,
duration_ms: None,
});
self
}
pub fn detail<V: Serialize>(mut self, key: &str, value: V) -> Self {
if let Ok(json_value) = serde_json::to_value(value) {
self.details.insert(key.to_string(), json_value);
}
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn organization(mut self, organization_id: &str) -> Self {
self.organization_id = Some(organization_id.to_string());
self
}
pub fn correlation(mut self, correlation_id: &str) -> Self {
self.correlation_id = Some(correlation_id.to_string());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn tags(mut self, tags: Vec<String>) -> Self {
self.tags.extend(tags);
self
}
pub fn build(self) -> Result<AuditEvent, String> {
let category = self.category.ok_or("Category is required")?;
let action = self.action.ok_or("Action is required")?;
let description = self
.description
.unwrap_or_else(|| format!("{:?} - {:?}", category, action));
Ok(AuditEvent {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now(),
category,
action,
outcome: self.outcome,
actor: self.actor,
resource: self.resource,
request: self.request,
details: self.details,
description,
error: self.error,
organization_id: self.organization_id,
correlation_id: self.correlation_id,
tags: self.tags,
})
}
}
pub struct AuditService {
db: Database,
buffer: Arc<RwLock<Vec<AuditEvent>>>,
buffer_size: usize,
enabled: bool,
categories_filter: Vec<AuditCategory>,
log_failures_only: bool,
}
impl AuditService {
pub fn new(db: Database) -> Self {
Self {
db,
buffer: Arc::new(RwLock::new(Vec::new())),
buffer_size: 100,
enabled: true,
categories_filter: vec![],
log_failures_only: false,
}
}
pub fn with_buffer_size(mut self, size: usize) -> Self {
self.buffer_size = size;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn categories(mut self, categories: Vec<AuditCategory>) -> Self {
self.categories_filter = categories;
self
}
pub fn failures_only(mut self) -> Self {
self.log_failures_only = true;
self
}
pub async fn log(&self, event: AuditEvent) {
if !self.enabled {
return;
}
if !self.categories_filter.is_empty() && !self.categories_filter.contains(&event.category) {
return;
}
if self.log_failures_only && event.outcome == AuditOutcome::Success {
return;
}
let mut buffer = self.buffer.write().await;
buffer.push(event);
if buffer.len() >= self.buffer_size {
let events: Vec<_> = buffer.drain(..).collect();
drop(buffer);
if let Err(e) = self.flush_events(events).await {
eprintln!("Failed to flush audit events: {}", e);
}
}
}
pub async fn log_builder(&self, builder: AuditEventBuilder) {
match builder.build() {
Ok(event) => self.log(event).await,
Err(e) => eprintln!("Failed to build audit event: {}", e),
}
}
pub async fn flush(&self) {
let mut buffer = self.buffer.write().await;
if buffer.is_empty() {
return;
}
let events: Vec<_> = buffer.drain(..).collect();
drop(buffer);
if let Err(e) = self.flush_events(events).await {
eprintln!("Failed to flush audit events: {}", e);
}
}
async fn flush_events(&self, events: Vec<AuditEvent>) -> Result<(), String> {
self.db
.insert_audit_events(&events)
.map_err(|e| format!("Database error: {}", e))
}
pub async fn query(&self, query: AuditQuery) -> Result<AuditQueryResult, String> {
self.db
.query_audit_events(&query)
.map_err(|e| format!("Database error: {}", e))
}
pub async fn get_event(&self, event_id: &str) -> Result<Option<AuditEvent>, String> {
self.db
.get_audit_event(event_id)
.map_err(|e| format!("Database error: {}", e))
}
pub async fn get_resource_history(
&self,
resource_type: &str,
resource_id: &str,
limit: Option<usize>,
) -> Result<Vec<AuditEvent>, String> {
self.db
.get_audit_events_for_resource(resource_type, resource_id, limit.unwrap_or(100))
.map_err(|e| format!("Database error: {}", e))
}
pub async fn get_user_activity(
&self,
user_id: &str,
from: Option<DateTime<Utc>>,
to: Option<DateTime<Utc>>,
limit: Option<usize>,
) -> Result<Vec<AuditEvent>, String> {
self.db
.get_audit_events_for_user(user_id, from, to, limit.unwrap_or(100))
.map_err(|e| format!("Database error: {}", e))
}
pub async fn export(&self, query: AuditQuery, format: ExportFormat) -> Result<Vec<u8>, String> {
let result = self.query(query).await?;
match format {
ExportFormat::Json => serde_json::to_vec_pretty(&result.events)
.map_err(|e| format!("JSON serialization error: {}", e)),
ExportFormat::Csv => self.events_to_csv(&result.events),
ExportFormat::JsonLines => {
let mut output = Vec::new();
for event in &result.events {
let line = serde_json::to_vec(event)
.map_err(|e| format!("JSON serialization error: {}", e))?;
output.extend(line);
output.push(b'\n');
}
Ok(output)
}
}
}
fn events_to_csv(&self, events: &[AuditEvent]) -> Result<Vec<u8>, String> {
let mut output = String::new();
output.push_str("id,timestamp,category,action,outcome,actor_id,actor_email,resource_type,resource_id,description,error\n");
for event in events {
let actor_id = event
.actor
.as_ref()
.map(|a| &a.user_id)
.unwrap_or(&String::new());
let actor_email = event
.actor
.as_ref()
.map(|a| &a.email)
.unwrap_or(&String::new());
let resource_type = event
.resource
.as_ref()
.map(|r| &r.resource_type)
.unwrap_or(&String::new());
let resource_id = event
.resource
.as_ref()
.map(|r| &r.resource_id)
.unwrap_or(&String::new());
let error = event.error.as_ref().unwrap_or(&String::new());
output.push_str(&format!(
"{},{},{},{},{},{},{},{},{},{},{}\n",
event.id,
event.timestamp.to_rfc3339(),
event.category.as_str(),
format!("{:?}", event.action),
format!("{:?}", event.outcome),
csv_escape(actor_id),
csv_escape(actor_email),
resource_type,
resource_id,
csv_escape(&event.description),
csv_escape(error),
));
}
Ok(output.into_bytes())
}
}
fn csv_escape(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct AuditQuery {
pub category: Option<AuditCategory>,
pub action: Option<AuditAction>,
pub outcome: Option<AuditOutcome>,
pub actor_id: Option<String>,
pub actor_email: Option<String>,
pub resource_type: Option<String>,
pub resource_id: Option<String>,
pub organization_id: Option<String>,
pub correlation_id: Option<String>,
pub search: Option<String>,
pub from: Option<DateTime<Utc>>,
pub to: Option<DateTime<Utc>>,
pub tags: Option<Vec<String>>,
pub offset: Option<usize>,
pub limit: Option<usize>,
pub sort_order: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AuditQueryResult {
pub events: Vec<AuditEvent>,
pub total: usize,
pub offset: usize,
pub limit: usize,
pub has_more: bool,
}
#[derive(Debug, Clone, Copy, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ExportFormat {
#[default]
Json,
Csv,
JsonLines,
}
pub async fn query_audit_logs(
audit_service: web::Data<AuditService>,
query: web::Query<AuditQuery>,
) -> HttpResponse {
match audit_service.query(query.into_inner()).await {
Ok(result) => HttpResponse::Ok().json(result),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_audit_event(
audit_service: web::Data<AuditService>,
path: web::Path<String>,
) -> HttpResponse {
let event_id = path.into_inner();
match audit_service.get_event(&event_id).await {
Ok(Some(event)) => HttpResponse::Ok().json(event),
Ok(None) => {
HttpResponse::NotFound().json(serde_json::json!({ "error": "Event not found" }))
}
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_resource_audit_history(
audit_service: web::Data<AuditService>,
path: web::Path<(String, String)>,
query: web::Query<HashMap<String, String>>,
) -> HttpResponse {
let (resource_type, resource_id) = path.into_inner();
let limit = query.get("limit").and_then(|s| s.parse().ok());
match audit_service
.get_resource_history(&resource_type, &resource_id, limit)
.await
{
Ok(events) => HttpResponse::Ok().json(events),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_user_audit_history(
audit_service: web::Data<AuditService>,
path: web::Path<String>,
query: web::Query<HashMap<String, String>>,
) -> HttpResponse {
let user_id = path.into_inner();
let from = query
.get("from")
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc));
let to = query
.get("to")
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc));
let limit = query.get("limit").and_then(|s| s.parse().ok());
match audit_service
.get_user_activity(&user_id, from, to, limit)
.await
{
Ok(events) => HttpResponse::Ok().json(events),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
#[derive(Debug, Deserialize)]
pub struct ExportQuery {
#[serde(flatten)]
pub query: AuditQuery,
pub format: Option<ExportFormat>,
}
pub async fn export_audit_logs(
audit_service: web::Data<AuditService>,
request: web::Json<ExportQuery>,
) -> HttpResponse {
let format = request.format.unwrap_or_default();
let content_type = match format {
ExportFormat::Json => "application/json",
ExportFormat::Csv => "text/csv",
ExportFormat::JsonLines => "application/x-ndjson",
};
match audit_service
.export(request.into_inner().query, format)
.await
{
Ok(data) => HttpResponse::Ok()
.content_type(content_type)
.append_header((
"Content-Disposition",
"attachment; filename=\"audit-log.export\"",
))
.body(data),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub fn configure_audit_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/audit")
.route("", web::get().to(query_audit_logs))
.route("/{event_id}", web::get().to(get_audit_event))
.route(
"/resource/{type}/{id}",
web::get().to(get_resource_audit_history),
)
.route("/user/{user_id}", web::get().to(get_user_audit_history))
.route("/export", web::post().to(export_audit_logs)),
);
}
#[macro_export]
macro_rules! audit {
($service:expr, $category:expr, $action:expr, $description:expr) => {
$service.log_builder(
$crate::api::audit::AuditEventBuilder::new()
.category($category)
.action($action)
.description($description)
.success()
).await
};
($service:expr, $category:expr, $action:expr, $description:expr, $($key:expr => $value:expr),*) => {
$service.log_builder(
$crate::api::audit::AuditEventBuilder::new()
.category($category)
.action($action)
.description($description)
.success()
$(.detail($key, $value))*
).await
};
}