1use std::sync::Arc;
2
3use teaql_core::{Record, Value};
4
5use crate::{RuntimeError, UserContext};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum RawAuditEventKind {
9 Created,
10 Updated,
11 Deleted,
12 Recovered,
13 SchemaCreated,
15 SchemaVerified,
17 FieldAdded,
19 DataSeeded,
21}
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct EntityPropertyChange {
25 pub field: String,
26 pub old_value: Option<Value>,
27 pub new_value: Option<Value>,
28}
29
30impl EntityPropertyChange {
31 pub fn new(
32 field: impl Into<String>,
33 old_value: Option<Value>,
34 new_value: Option<Value>,
35 ) -> Self {
36 Self {
37 field: field.into(),
38 old_value,
39 new_value,
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct RawAuditEvent {
46 pub kind: RawAuditEventKind,
47 pub entity: String,
48 pub values: Record,
49 pub updated_fields: Vec<String>,
50 pub old_values: Option<Record>,
51 pub new_values: Option<Record>,
52 pub changes: Vec<EntityPropertyChange>,
53 pub trace_chain: Vec<teaql_core::TraceNode>,
55}
56
57impl RawAuditEvent {
58 pub fn created(entity: impl Into<String>, values: Record) -> Self {
59 let changes = values
60 .iter()
61 .map(|(field, value)| {
62 EntityPropertyChange::new(field.clone(), None, Some(value.clone()))
63 })
64 .collect();
65 Self {
66 kind: RawAuditEventKind::Created,
67 entity: entity.into(),
68 values: values.clone(),
69 updated_fields: Vec::new(),
70 old_values: None,
71 new_values: Some(values),
72 changes,
73 trace_chain: Vec::new(),
74 }
75 }
76
77 pub fn updated(entity: impl Into<String>, values: Record) -> Self {
78 let updated_fields = values.keys().cloned().collect::<Vec<_>>();
79 let changes = Self::changes_for_fields(None, Some(&values), &updated_fields);
80 Self {
81 kind: RawAuditEventKind::Updated,
82 entity: entity.into(),
83 values: values.clone(),
84 updated_fields,
85 old_values: None,
86 new_values: Some(values),
87 changes,
88 trace_chain: Vec::new(),
89 }
90 }
91
92 pub fn updated_with_old_values(
93 entity: impl Into<String>,
94 values: Record,
95 old_values: Option<Record>,
96 new_values: Record,
97 updated_fields: Vec<String>,
98 ) -> Self {
99 let changes =
100 Self::changes_for_fields(old_values.as_ref(), Some(&new_values), &updated_fields);
101 Self {
102 kind: RawAuditEventKind::Updated,
103 entity: entity.into(),
104 values,
105 updated_fields,
106 old_values,
107 new_values: Some(new_values),
108 changes,
109 trace_chain: Vec::new(),
110 }
111 }
112
113 pub fn deleted(entity: impl Into<String>, id: Value, expected_version: Option<i64>) -> Self {
114 let mut values = Record::from([("id".to_owned(), id)]);
115 if let Some(version) = expected_version {
116 values.insert("version".to_owned(), Value::I64(version));
117 }
118 Self {
119 kind: RawAuditEventKind::Deleted,
120 entity: entity.into(),
121 values,
122 updated_fields: Vec::new(),
123 old_values: None,
124 new_values: None,
125 changes: Vec::new(),
126 trace_chain: Vec::new(),
127 }
128 }
129
130 pub fn deleted_with_old_values(
131 entity: impl Into<String>,
132 id: Value,
133 expected_version: Option<i64>,
134 old_values: Option<Record>,
135 ) -> Self {
136 let mut event = Self::deleted(entity, id, expected_version);
137 event.changes = old_values
138 .as_ref()
139 .map(|values| {
140 values
141 .iter()
142 .map(|(field, value)| {
143 EntityPropertyChange::new(field.clone(), Some(value.clone()), None)
144 })
145 .collect()
146 })
147 .unwrap_or_default();
148 event.old_values = old_values;
149 event
150 }
151
152 pub fn recovered(entity: impl Into<String>, id: Value, expected_version: i64) -> Self {
153 let values = Record::from([
154 ("id".to_owned(), id),
155 ("version".to_owned(), Value::I64(expected_version)),
156 ]);
157 Self {
158 kind: RawAuditEventKind::Recovered,
159 entity: entity.into(),
160 values,
161 updated_fields: Vec::new(),
162 old_values: None,
163 new_values: None,
164 changes: Vec::new(),
165 trace_chain: Vec::new(),
166 }
167 }
168
169 pub fn recovered_with_old_values(
170 entity: impl Into<String>,
171 id: Value,
172 expected_version: i64,
173 old_values: Option<Record>,
174 ) -> Self {
175 let recovered_version = -expected_version + 1;
176 let mut new_values = old_values.clone().unwrap_or_default();
177 new_values.insert("id".to_owned(), id.clone());
178 new_values.insert("version".to_owned(), Value::I64(recovered_version));
179 let mut event = Self::recovered(entity, id, expected_version);
180 event.old_values = old_values;
181 event.new_values = Some(new_values.clone());
182 event.changes = Self::changes_for_fields(
183 event.old_values.as_ref(),
184 Some(&new_values),
185 &["version".to_owned()],
186 );
187 event
188 }
189
190 pub fn schema_created(
192 entity: impl Into<String>,
193 table_name: impl Into<String>,
194 field_count: usize,
195 ) -> Self {
196 let entity = entity.into();
197 let values = Record::from([
198 ("table_name".to_owned(), Value::Text(table_name.into())),
199 ("field_count".to_owned(), Value::I64(field_count as i64)),
200 ]);
201 let changes = values
202 .iter()
203 .map(|(k, v)| EntityPropertyChange::new(k.clone(), None, Some(v.clone())))
204 .collect();
205 Self {
206 kind: RawAuditEventKind::SchemaCreated,
207 entity,
208 values,
209 updated_fields: Vec::new(),
210 old_values: None,
211 new_values: None,
212 changes,
213 trace_chain: Vec::new(),
214 }
215 }
216
217 pub fn schema_verified(
219 entity: impl Into<String>,
220 table_name: impl Into<String>,
221 field_count: usize,
222 ) -> Self {
223 let entity = entity.into();
224 let values = Record::from([
225 ("table_name".to_owned(), Value::Text(table_name.into())),
226 ("field_count".to_owned(), Value::I64(field_count as i64)),
227 ]);
228 let changes = values
229 .iter()
230 .map(|(k, v)| EntityPropertyChange::new(k.clone(), None, Some(v.clone())))
231 .collect();
232 Self {
233 kind: RawAuditEventKind::SchemaVerified,
234 entity,
235 values,
236 updated_fields: Vec::new(),
237 old_values: None,
238 new_values: None,
239 changes,
240 trace_chain: Vec::new(),
241 }
242 }
243
244 pub fn field_added(
246 entity: impl Into<String>,
247 table_name: impl Into<String>,
248 field_name: impl Into<String>,
249 ) -> Self {
250 let entity = entity.into();
251 let values = Record::from([
252 ("table_name".to_owned(), Value::Text(table_name.into())),
253 ("field_name".to_owned(), Value::Text(field_name.into())),
254 ]);
255 let changes = values
256 .iter()
257 .map(|(k, v)| EntityPropertyChange::new(k.clone(), None, Some(v.clone())))
258 .collect();
259 Self {
260 kind: RawAuditEventKind::FieldAdded,
261 entity,
262 values,
263 updated_fields: Vec::new(),
264 old_values: None,
265 new_values: None,
266 changes,
267 trace_chain: Vec::new(),
268 }
269 }
270
271 pub fn data_seeded(
273 entity: impl Into<String>,
274 table_name: impl Into<String>,
275 inserted: usize,
276 updated: usize,
277 ) -> Self {
278 let entity = entity.into();
279 let values = Record::from([
280 ("table_name".to_owned(), Value::Text(table_name.into())),
281 ("inserted".to_owned(), Value::I64(inserted as i64)),
282 ("updated".to_owned(), Value::I64(updated as i64)),
283 ]);
284 let changes = values
285 .iter()
286 .map(|(k, v)| EntityPropertyChange::new(k.clone(), None, Some(v.clone())))
287 .collect();
288 Self {
289 kind: RawAuditEventKind::DataSeeded,
290 entity,
291 values,
292 updated_fields: Vec::new(),
293 old_values: None,
294 new_values: None,
295 changes,
296 trace_chain: Vec::new(),
297 }
298 }
299
300 fn changes_for_fields(
301 old_values: Option<&Record>,
302 new_values: Option<&Record>,
303 fields: &[String],
304 ) -> Vec<EntityPropertyChange> {
305 fields
306 .iter()
307 .map(|field| {
308 EntityPropertyChange::new(
309 field.clone(),
310 old_values.and_then(|values| values.get(field).cloned()),
311 new_values.and_then(|values| values.get(field).cloned()),
312 )
313 })
314 .collect()
315 }
316
317 pub fn build_safe_event(
318 &self,
319 audit_mask_fields: &[String],
320 audit_value_max_len: Option<usize>,
321 ) -> SafeAuditEvent {
322 let mut safe_fields = Vec::new();
323 for change in &self.changes {
324 if change.field.starts_with('_') {
325 continue;
326 }
327 let raw_val_str = change.new_value.as_ref().map(|v| format!("{:?}", v));
331 let safe_field = build_safe_audit_field(
332 &change.field,
333 raw_val_str.as_deref(),
334 audit_mask_fields,
335 audit_value_max_len,
336 );
337 safe_fields.push(safe_field);
338 }
339
340 SafeAuditEvent {
341 kind: self.kind,
342 entity: self.entity.clone(),
343 fields: safe_fields,
344 trace_chain: self.trace_chain.clone(),
345 }
346 }
347}
348
349pub fn mask_audit_value(value: &str) -> String {
350 let chars: Vec<char> = value.chars().collect();
351 let len = chars.len();
352
353 if len == 0 {
354 return String::new();
355 }
356
357 if chars.iter().all(|c| c.is_ascii_digit()) {
358 return "*".repeat(len);
359 }
360
361 if len < 8 {
362 return "*".repeat(len);
363 }
364
365 let prefix: String = chars[0..2].iter().collect();
366 let suffix: String = chars[len - 2..len].iter().collect();
367 let middle = "*".repeat(len - 4);
368
369 format!("{}{}{}", prefix, middle, suffix)
370}
371
372pub fn limit_audit_value(value: &str, max_len: usize) -> (String, bool) {
373 let chars: Vec<char> = value.chars().collect();
374 let len = chars.len();
375
376 if len <= max_len {
377 return (value.to_string(), false);
378 }
379
380 if max_len <= 3 {
381 return ("*".repeat(max_len), true);
382 }
383
384 let marker = "...";
385 let keep_len = max_len - marker.len();
386 let head_len = keep_len / 2;
387 let tail_len = keep_len - head_len;
388
389 let head: String = chars[0..head_len].iter().collect();
390 let tail: String = chars[len - tail_len..len].iter().collect();
391
392 (format!("{}{}{}", head, marker, tail), true)
393}
394
395pub fn build_safe_audit_field(
396 field_name: &str,
397 raw_value: Option<&str>,
398 audit_mask_fields: &[String],
399 audit_value_max_len: Option<usize>,
400) -> SafeAuditField {
401 match raw_value {
402 None => SafeAuditField {
403 name: field_name.to_string(),
404 value: None,
405 masked: false,
406 truncated: false,
407 raw_length: None,
408 output_length: None,
409 mask_reason: None,
410 truncate_reason: None,
411 },
412 Some(raw) => {
413 let raw_length = raw.chars().count();
414 let should_mask = audit_mask_fields.iter().any(|f| f == field_name);
415
416 let mut value = match should_mask {
417 true => mask_audit_value(raw),
418 false => raw.to_string(),
419 };
420
421 let mut truncated = false;
422 if let Some(max_len) = audit_value_max_len {
423 let result = limit_audit_value(&value, max_len);
424 value = result.0;
425 truncated = result.1;
426 }
427
428 let output_length = value.chars().count();
429
430 SafeAuditField {
431 name: field_name.to_string(),
432 value: Some(value),
433 masked: should_mask,
434 truncated,
435 raw_length: Some(raw_length),
436 output_length: Some(output_length),
437 mask_reason: should_mask.then(|| "_audit_mask_fields".to_string()),
438 truncate_reason: truncated.then(|| "_audit_value_max_len".to_string()),
439 }
440 }
441 }
442}
443
444pub trait RawAuditEventSink: Send + Sync {
445 fn on_event(&self, ctx: &UserContext, event: &RawAuditEvent) -> Result<(), RuntimeError>;
446}
447
448#[derive(Default, Clone)]
449pub struct InMemoryRawAuditEventSink {
450 sinks: Vec<Arc<dyn RawAuditEventSink>>,
451}
452
453impl InMemoryRawAuditEventSink {
454 pub fn new() -> Self {
455 Self::default()
456 }
457
458 pub fn register(&mut self, sink: impl RawAuditEventSink + 'static) {
459 self.sinks.push(Arc::new(sink));
460 }
461
462 pub fn with_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
463 self.register(sink);
464 self
465 }
466}
467
468impl RawAuditEventSink for InMemoryRawAuditEventSink {
469 fn on_event(&self, ctx: &UserContext, event: &RawAuditEvent) -> Result<(), RuntimeError> {
470 for sink in &self.sinks {
471 sink.on_event(ctx, event)?;
472 }
473 Ok(())
474 }
475}
476
477#[derive(Debug, Clone, PartialEq)]
478pub struct SafeAuditField {
479 pub name: String,
480 pub value: Option<String>,
481 pub masked: bool,
482 pub truncated: bool,
483 pub raw_length: Option<usize>,
484 pub output_length: Option<usize>,
485 pub mask_reason: Option<String>,
486 pub truncate_reason: Option<String>,
487}
488
489#[derive(Debug, Clone, PartialEq)]
490pub struct SafeAuditEvent {
491 pub kind: RawAuditEventKind,
492 pub entity: String,
493 pub fields: Vec<SafeAuditField>,
494 pub trace_chain: Vec<teaql_core::TraceNode>,
495}
496
497pub trait SafeAuditEventSink: Send + Sync {
498 fn on_safe_event(
499 &self,
500 ctx: &crate::UserContext,
501 event: &SafeAuditEvent,
502 ) -> Result<(), crate::RuntimeError>;
503}