mod arrow_ipc;
pub use arrow_ipc::ArrowIpcAuditLog;
use std::fmt;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditAction {
Download,
Upload,
Delete,
List,
Promote,
Other,
}
impl AuditAction {
pub fn as_str(&self) -> &'static str {
match self {
AuditAction::Download => "download",
AuditAction::Upload => "upload",
AuditAction::Delete => "delete",
AuditAction::List => "list",
AuditAction::Promote => "promote",
AuditAction::Other => "other",
}
}
pub fn from_wire(s: &str) -> Self {
match s {
"download" => AuditAction::Download,
"upload" => AuditAction::Upload,
"delete" => AuditAction::Delete,
"list" => AuditAction::List,
"promote" => AuditAction::Promote,
_ => AuditAction::Other,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditEvent {
pub ts_nanos: i64,
pub ident: String,
pub action: AuditAction,
pub repo: String,
pub artifact: String,
pub source_ip: String,
pub status: u16,
pub bytes: u64,
pub detail: String,
}
impl AuditEvent {
#[allow(clippy::too_many_arguments)]
pub fn new(
ident: impl Into<String>,
action: AuditAction,
repo: impl Into<String>,
artifact: impl Into<String>,
source_ip: impl Into<String>,
status: u16,
bytes: u64,
) -> Self {
Self {
ts_nanos: now_nanos(),
ident: ident.into(),
action,
repo: repo.into(),
artifact: artifact.into(),
source_ip: source_ip.into(),
status,
bytes,
detail: String::new(),
}
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = detail.into();
self
}
}
pub fn now_nanos() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as i64)
.unwrap_or(0)
}
pub trait AuditLog: Send + Sync {
fn record(&self, event: AuditEvent) -> Result<(), AuditError>;
fn flush(&self) -> Result<(), AuditError> {
Ok(())
}
}
pub fn default_audit_log(dir: impl AsRef<Path>) -> Result<Arc<dyn AuditLog>, AuditError> {
Ok(Arc::new(ArrowIpcAuditLog::new(dir)?))
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopAuditLog;
impl AuditLog for NoopAuditLog {
fn record(&self, _event: AuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
#[derive(Debug, Default)]
pub struct MemoryAuditLog {
events: Mutex<Vec<AuditEvent>>,
}
impl MemoryAuditLog {
pub fn new() -> Self {
Self::default()
}
pub fn events(&self) -> Vec<AuditEvent> {
self.events.lock().expect("audit mutex poisoned").clone()
}
}
impl AuditLog for MemoryAuditLog {
fn record(&self, event: AuditEvent) -> Result<(), AuditError> {
self.events.lock().expect("audit mutex poisoned").push(event);
Ok(())
}
}
#[derive(Debug)]
pub enum AuditError {
Io(std::io::Error),
Arrow(arrow_schema::ArrowError),
Other(String),
}
impl fmt::Display for AuditError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuditError::Io(e) => write!(f, "audit io error: {e}"),
AuditError::Arrow(e) => write!(f, "audit arrow error: {e}"),
AuditError::Other(s) => write!(f, "audit error: {s}"),
}
}
}
impl std::error::Error for AuditError {}
impl From<std::io::Error> for AuditError {
fn from(e: std::io::Error) -> Self {
AuditError::Io(e)
}
}
impl From<arrow_schema::ArrowError> for AuditError {
fn from(e: arrow_schema::ArrowError) -> Self {
AuditError::Arrow(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn action_wire_roundtrips() {
for a in [
AuditAction::Download,
AuditAction::Upload,
AuditAction::Delete,
AuditAction::List,
AuditAction::Promote,
AuditAction::Other,
] {
assert_eq!(AuditAction::from_wire(a.as_str()), a);
}
assert_eq!(AuditAction::from_wire("teleport"), AuditAction::Other);
}
#[test]
fn memory_log_collects_in_order() {
let log = MemoryAuditLog::new();
log.record(AuditEvent::new("alice", AuditAction::Upload, "crates", "serde@1", "10.0.0.1:5", 200, 42))
.unwrap();
log.record(AuditEvent::new("anonymous", AuditAction::Download, "crates", "serde@1", "10.0.0.2:6", 200, 42))
.unwrap();
let evs = log.events();
assert_eq!(evs.len(), 2);
assert_eq!(evs[0].ident, "alice");
assert_eq!(evs[0].action, AuditAction::Upload);
assert_eq!(evs[1].ident, "anonymous");
}
#[test]
fn noop_log_is_inert() {
let log = NoopAuditLog;
log.record(AuditEvent::new("x", AuditAction::List, "r", "", "", 200, 0)).unwrap();
log.flush().unwrap();
}
}