use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{LazyLock, Mutex, RwLock};
use serde_json::Value;
use crate::action::Action;
use crate::changes::REDACTED;
pub const DEFAULT_IGNORED_ATTRIBUTES: &[&str] = &[
"lock_version",
"created_at",
"updated_at",
"created_on",
"updated_on",
];
#[derive(Clone, Debug)]
pub struct GlobalConfig {
pub ignored_attributes: Vec<String>,
pub max_audits: Option<usize>,
}
impl Default for GlobalConfig {
fn default() -> Self {
GlobalConfig {
ignored_attributes: DEFAULT_IGNORED_ATTRIBUTES
.iter()
.map(|s| s.to_string())
.collect(),
max_audits: None,
}
}
}
static GLOBAL: LazyLock<RwLock<GlobalConfig>> =
LazyLock::new(|| RwLock::new(GlobalConfig::default()));
static GLOBAL_ENABLED: AtomicBool = AtomicBool::new(true);
static TYPE_ENABLED: LazyLock<Mutex<HashMap<String, bool>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn config(f: impl FnOnce(&mut GlobalConfig)) {
let mut g = GLOBAL.write().expect("auditlog global config poisoned");
f(&mut g);
}
pub fn global_config() -> GlobalConfig {
GLOBAL
.read()
.expect("auditlog global config poisoned")
.clone()
}
pub fn global_max_audits() -> Option<usize> {
GLOBAL
.read()
.expect("auditlog global config poisoned")
.max_audits
}
pub fn set_auditing_enabled(enabled: bool) {
GLOBAL_ENABLED.store(enabled, Ordering::SeqCst);
}
pub fn auditing_enabled() -> bool {
GLOBAL_ENABLED.load(Ordering::SeqCst)
}
pub fn set_type_enabled(auditable_type: &str, enabled: bool) {
TYPE_ENABLED
.lock()
.expect("auditlog type-enabled poisoned")
.insert(auditable_type.to_string(), enabled);
}
pub fn type_enabled(auditable_type: &str) -> bool {
TYPE_ENABLED
.lock()
.expect("auditlog type-enabled poisoned")
.get(auditable_type)
.copied()
.unwrap_or(true)
}
#[derive(Clone, Debug)]
pub struct AuditOptions {
pub only: Option<Vec<String>>,
pub except: Option<Vec<String>>,
pub on: Vec<Action>,
pub comment_required: bool,
pub update_with_comment_only: bool,
pub max_audits: Option<usize>,
pub redacted: Vec<String>,
pub redaction_value: Value,
pub encrypted: Vec<String>,
pub associated_with: Option<String>,
}
impl Default for AuditOptions {
fn default() -> Self {
AuditOptions {
only: None,
except: None,
on: vec![Action::Create, Action::Update, Action::Destroy],
comment_required: false,
update_with_comment_only: true,
max_audits: None,
redacted: Vec::new(),
redaction_value: Value::String(REDACTED.to_string()),
encrypted: Vec::new(),
associated_with: None,
}
}
}
impl AuditOptions {
pub fn builder() -> AuditOptionsBuilder {
AuditOptionsBuilder {
options: AuditOptions::default(),
}
}
pub fn effective_max_audits(&self) -> Option<usize> {
self.max_audits.or_else(global_max_audits)
}
pub fn audits_action(&self, action: Action) -> bool {
self.on.contains(&action)
}
pub fn non_audited_columns(
&self,
all_columns: &[String],
primary_key: &str,
inheritance_column: Option<&str>,
) -> HashSet<String> {
let mut default_ignored: HashSet<String> =
global_config().ignored_attributes.into_iter().collect();
default_ignored.insert(primary_key.to_string());
if let Some(type_col) = inheritance_column {
default_ignored.insert(type_col.to_string());
}
if let Some(only) = &self.only {
let only_set: HashSet<&String> = only.iter().collect();
let mut result: HashSet<String> = all_columns.iter().cloned().collect();
result.extend(default_ignored.iter().cloned());
result.retain(|c| !only_set.contains(c));
result
} else if let Some(except) = &self.except {
default_ignored.extend(except.iter().cloned());
default_ignored
} else {
default_ignored
}
}
pub(crate) fn filter_attributes(
&self,
attrs: &crate::changes::ValueMap,
primary_key: &str,
inheritance_column: Option<&str>,
) -> crate::changes::ValueMap {
let all_columns: Vec<String> = attrs.keys().cloned().collect();
let non_audited = self.non_audited_columns(&all_columns, primary_key, inheritance_column);
attrs
.iter()
.filter(|(k, _)| !non_audited.contains(*k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
}
#[derive(Clone, Debug)]
pub struct AuditOptionsBuilder {
options: AuditOptions,
}
impl AuditOptionsBuilder {
pub fn only<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options.only = Some(columns.into_iter().map(Into::into).collect());
self.options.except = None;
self
}
pub fn except<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options.except = Some(columns.into_iter().map(Into::into).collect());
self.options.only = None;
self
}
pub fn on<I>(mut self, actions: I) -> Self
where
I: IntoIterator<Item = Action>,
{
self.options.on = actions.into_iter().collect();
self
}
pub fn comment_required(mut self, required: bool) -> Self {
self.options.comment_required = required;
self
}
pub fn update_with_comment_only(mut self, allowed: bool) -> Self {
self.options.update_with_comment_only = allowed;
self
}
pub fn max_audits(mut self, max: usize) -> Self {
self.options.max_audits = Some(max);
self
}
pub fn redacted<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options.redacted = columns.into_iter().map(Into::into).collect();
self
}
pub fn redaction_value(mut self, value: impl Into<Value>) -> Self {
self.options.redaction_value = value.into();
self
}
pub fn encrypted<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options.encrypted = columns.into_iter().map(Into::into).collect();
self
}
pub fn associated_with(mut self, type_name: impl Into<String>) -> Self {
self.options.associated_with = Some(type_name.into());
self
}
pub fn build(self) -> AuditOptions {
assert!(
!(self.options.only.is_some() && self.options.except.is_some()),
"audited options `only` and `except` are mutually exclusive"
);
assert!(
!self.options.on.is_empty(),
"audited options `on` must list at least one action"
);
self.options
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cols(c: &[&str]) -> Vec<String> {
c.iter().map(|s| s.to_string()).collect()
}
#[test]
fn non_audited_default_includes_pk_and_timestamps() {
let opts = AuditOptions::default();
let na = opts.non_audited_columns(&cols(&["id", "name", "created_at"]), "id", None);
assert!(na.contains("id"));
assert!(na.contains("created_at"));
assert!(!na.contains("name"));
}
#[test]
fn non_audited_includes_sti_inheritance_column() {
let opts = AuditOptions::default();
let na = opts.non_audited_columns(&cols(&["id", "type", "name"]), "id", Some("type"));
assert!(na.contains("type"));
assert!(!na.contains("name"));
}
#[test]
fn only_audits_just_those_columns() {
let opts = AuditOptions::builder().only(["name"]).build();
let na = opts.non_audited_columns(&cols(&["id", "name", "email"]), "id", None);
assert!(na.contains("id"));
assert!(na.contains("email"));
assert!(!na.contains("name"));
}
#[test]
fn except_excludes_extra_columns() {
let opts = AuditOptions::builder().except(["password"]).build();
let na = opts.non_audited_columns(&cols(&["id", "name", "password"]), "id", None);
assert!(na.contains("password"));
assert!(na.contains("id"));
assert!(!na.contains("name"));
}
#[test]
fn only_and_except_are_mutually_exclusive() {
let opts = AuditOptions::builder().except(["a"]).only(["b"]).build();
assert!(opts.except.is_none());
assert_eq!(opts.only, Some(vec!["b".to_string()]));
}
}