Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions acton-service/src/audit/storage/clickhouse_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,7 @@ impl From<AuditQueryRow> for AuditEvent {
"config.drift_detected" => AuditEventKind::ConfigDriftDetected,
"http.request" => AuditEventKind::HttpRequest,
"http.request.denied" => AuditEventKind::HttpRequestDenied,
other => {
let name = other.strip_prefix("custom.").unwrap_or(other);
AuditEventKind::Custom(name.to_string())
}
other => AuditEventKind::Custom(super::parse_custom_kind(other)),
};

let severity = match row.severity {
Expand Down
66 changes: 66 additions & 0 deletions acton-service/src/audit/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,72 @@ pub mod surrealdb_impl;
#[cfg(feature = "clickhouse")]
pub mod clickhouse_impl;

/// Returns true if the stored event-kind string looks like a framework-owned
/// kind (`auth.*`, `http.*`, `account.*`, `config.*`) that should have been
/// recognized by the parser. Used by parser catch-alls to detect likely
/// version skew between an emitter and a reader.
pub(crate) fn looks_like_framework_kind(s: &str) -> bool {
s.starts_with("auth.")
|| s.starts_with("http.")
|| s.starts_with("account.")
|| s.starts_with("config.")
}

/// Helper for storage-backend parser catch-alls.
///
/// Strips the `custom.` prefix when present (so user-defined custom events
/// round-trip cleanly) and emits a `tracing::warn!` when the input looks
/// like a framework-owned kind that no parser arm matched — i.e. the
/// emitter is on a newer version than this reader.
pub(crate) fn parse_custom_kind(s: &str) -> String {
if looks_like_framework_kind(s) {
tracing::warn!(
stored_kind = %s,
"unrecognized framework audit event kind — falling back to Custom; likely version skew between emitter and reader"
);
}
s.strip_prefix("custom.").unwrap_or(s).to_string()
}

#[cfg(test)]
mod helper_tests {
use super::{looks_like_framework_kind, parse_custom_kind};

#[test]
fn framework_prefixes_detected() {
assert!(looks_like_framework_kind("auth.token.invalid"));
assert!(looks_like_framework_kind("http.request.denied"));
assert!(looks_like_framework_kind("account.created"));
assert!(looks_like_framework_kind("config.drift_detected"));
}

#[test]
fn non_framework_prefixes_ignored() {
assert!(!looks_like_framework_kind("custom.user.exported"));
assert!(!looks_like_framework_kind("user.signed_up"));
assert!(!looks_like_framework_kind("billing.invoice.paid"));
assert!(!looks_like_framework_kind(""));
}

#[test]
fn parse_custom_strips_custom_prefix() {
assert_eq!(parse_custom_kind("custom.user.exported"), "user.exported");
}

#[test]
fn parse_custom_preserves_unprefixed_user_strings() {
assert_eq!(parse_custom_kind("billing.invoice.paid"), "billing.invoice.paid");
}

#[test]
fn parse_custom_passes_through_framework_strings_for_visibility() {
// The warn fires (verified manually / via tracing subscribers in
// integration tests); we assert the returned string preserves the
// original so operators can grep for it in their event store.
assert_eq!(parse_custom_kind("auth.token.invalid"), "auth.token.invalid");
}
}

/// Trait for audit event persistence backends
///
/// Implementations MUST enforce append-only semantics at the database level
Expand Down
5 changes: 1 addition & 4 deletions acton-service/src/audit/storage/pg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,7 @@ impl From<AuditEventRow> for AuditEvent {
"config.drift_detected" => AuditEventKind::ConfigDriftDetected,
"http.request" => AuditEventKind::HttpRequest,
"http.request.denied" => AuditEventKind::HttpRequestDenied,
other => {
let name = other.strip_prefix("custom.").unwrap_or(other);
AuditEventKind::Custom(name.to_string())
}
other => AuditEventKind::Custom(super::parse_custom_kind(other)),
};

let severity = match row.severity {
Expand Down
5 changes: 1 addition & 4 deletions acton-service/src/audit/storage/surrealdb_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,7 @@ fn parse_event_kind(s: &str) -> AuditEventKind {
"config.drift_detected" => AuditEventKind::ConfigDriftDetected,
"http.request" => AuditEventKind::HttpRequest,
"http.request.denied" => AuditEventKind::HttpRequestDenied,
other => {
let name = other.strip_prefix("custom.").unwrap_or(other);
AuditEventKind::Custom(name.to_string())
}
other => AuditEventKind::Custom(super::parse_custom_kind(other)),
}
}

Expand Down
5 changes: 1 addition & 4 deletions acton-service/src/audit/storage/turso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,7 @@ fn parse_event_kind(s: &str) -> AuditEventKind {
"config.drift_detected" => AuditEventKind::ConfigDriftDetected,
"http.request" => AuditEventKind::HttpRequest,
"http.request.denied" => AuditEventKind::HttpRequestDenied,
other => {
let name = other.strip_prefix("custom.").unwrap_or(other);
AuditEventKind::Custom(name.to_string())
}
other => AuditEventKind::Custom(super::parse_custom_kind(other)),
}
}

Expand Down
Loading