1use std::cell::RefCell;
4use std::collections::{BTreeMap, BTreeSet};
5use std::rc::Rc;
6
7use ravenrs_criteria::Value;
8
9use super::{SECURITY_AUDIT_ENTRY, SpaceMaker};
10use crate::space::key_text;
11use crate::{Change, EntityInfo, FieldInfo, FieldKind, Object, ObjectSpace, Query, SpaceError, SpaceWatcher};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15#[non_exhaustive]
16#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
17pub enum AuditMode {
18 #[default]
20 Full,
21 Lightweight,
23 CreationOnly,
25 Off,
27}
28
29#[derive(Clone, Debug, Default)]
31#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
32pub struct AuditSettings {
33 mode: AuditMode,
34 modes: BTreeMap<String, AuditMode>,
35 left_out: BTreeSet<(String, String)>,
36}
37
38#[cfg_attr(feature = "live", ravenrs_live::expose)]
39impl AuditSettings {
40 pub fn new(mode: AuditMode) -> Self {
42 Self { mode, ..Self::default() }
43 }
44
45 pub fn entity(mut self, entity: impl Into<String>, mode: AuditMode) -> Self {
47 self.modes.insert(entity.into(), mode);
48 self
49 }
50
51 pub fn without_field(mut self, entity: impl Into<String>, field: impl Into<String>) -> Self {
53 self.left_out.insert((entity.into(), field.into()));
54 self
55 }
56
57 fn mode_of(&self, entity: &str) -> AuditMode {
58 if entity == SECURITY_AUDIT_ENTRY {
59 return AuditMode::Off;
60 }
61 self.modes.get(entity).copied().unwrap_or(self.mode)
62 }
63
64 fn covers(&self, field: &FieldInfo, entity: &str) -> bool {
65 !field.kind.is_collection()
66 && field.calculated.is_none()
67 && !self.left_out.contains(&(entity.to_owned(), field.name.clone()))
68 }
69}
70
71#[derive(Clone, Debug)]
73struct Line {
74 change: &'static str,
75 entity: String,
76 key: String,
77 field: String,
78 old: Value,
79 new: Value,
80}
81
82#[cfg_attr(feature = "live", ravenrs_live::expose)]
83impl Line {
84 fn of(change: &'static str, entity: &str, key: &str) -> Self {
85 Self {
86 change,
87 entity: entity.to_owned(),
88 key: key.to_owned(),
89 field: String::new(),
90 old: Value::Null,
91 new: Value::Null,
92 }
93 }
94
95 fn field(mut self, name: &str) -> Self {
96 self.field = name.to_owned();
97 self
98 }
99
100 fn values(mut self, old: Value, new: Value) -> Self {
101 self.old = old;
102 self.new = new;
103 self
104 }
105}
106
107#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
109pub struct AuditTrail {
110 settings: AuditSettings,
111 author: Rc<dyn Fn() -> String>,
112 waiting: RefCell<Vec<Line>>,
113 writer: RefCell<Option<SpaceMaker>>,
115}
116
117impl std::fmt::Debug for AuditTrail {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("AuditTrail").field("settings", &self.settings).finish_non_exhaustive()
120 }
121}
122
123#[cfg_attr(feature = "live", ravenrs_live::expose)]
124impl AuditTrail {
125 pub fn new(settings: AuditSettings, author: impl Fn() -> String + 'static) -> Self {
127 Self { settings, author: Rc::new(author), waiting: RefCell::default(), writer: RefCell::default() }
128 }
129
130 pub fn write_with(&self, space: SpaceMaker) {
132 *self.writer.borrow_mut() = Some(space);
133 }
134
135 pub fn custom(&self, entity: &str, key: &Value, field: &str, what: &str) {
138 let line = Line::of("custom", entity, &key_text(key).unwrap_or_default())
139 .field(field)
140 .values(Value::Null, Value::Text(what.to_owned()));
141 let writer = self.writer.borrow().clone();
142 if let Some(writer) = writer {
143 let mut space = writer();
144 if self.write(&mut *space, std::slice::from_ref(&line)).is_ok() && space.commit().is_ok() {
145 return;
146 }
147 }
148 self.waiting.borrow_mut().push(line);
149 }
150
151 fn write(&self, space: &mut dyn ObjectSpace, lines: &[Line]) -> Result<Vec<Value>, SpaceError> {
153 let (author, now) = ((self.author)(), crate::local_now());
154 let mut added = Vec::with_capacity(lines.len());
155 for line in lines {
156 let mut entry = space.new_object(SECURITY_AUDIT_ENTRY)?;
157 let key = entry.get("id");
158 entry.set("happened_at", Value::Date(now));
159 entry.set("author", author.clone());
160 entry.set("change", line.change);
161 entry.set("target_entity", line.entity.clone());
162 entry.set("target_key", line.key.clone());
163 entry.set("field", line.field.clone());
164 entry.set("old_value", line.old.clone());
165 entry.set("new_value", line.new.clone());
166 space.insert(entry)?;
167 added.push(key);
168 }
169 Ok(added)
170 }
171}
172
173fn shown(field: &FieldInfo, value: &Value) -> Value {
175 if field.secret || matches!(field.kind, FieldKind::Image | FieldKind::File) {
176 return Value::Null;
177 }
178 match value {
179 Value::Null => Value::Null,
180 Value::Text(text) => Value::Text(text.clone()),
181 Value::Number(number) => Value::Text(number.to_string()),
182 Value::Bool(on) => Value::Text(on.to_string()),
183 Value::Date(date) => {
184 let (year, month, day) = date.ymd();
185 let (hour, minute) = (date.seconds / 3600, date.seconds % 3600 / 60);
186 Value::Text(format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}"))
187 }
188 _ => Value::Null,
189 }
190}
191
192#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
195pub struct AuditedObjectSpace {
196 inner: Box<dyn ObjectSpace>,
197 trail: Rc<AuditTrail>,
198 entities: Vec<EntityInfo>,
199 before: BTreeMap<(String, String), Option<Object>>,
201}
202
203impl std::fmt::Debug for AuditedObjectSpace {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.debug_struct("AuditedObjectSpace").field("trail", &self.trail).finish_non_exhaustive()
206 }
207}
208
209#[cfg_attr(feature = "live", ravenrs_live::expose)]
210impl AuditedObjectSpace {
211 pub fn new(inner: Box<dyn ObjectSpace>, trail: Rc<AuditTrail>) -> Self {
213 let entities = inner.entities();
214 Self { inner, trail, entities, before: BTreeMap::new() }
215 }
216
217 fn entity(&self, name: &str) -> Option<&EntityInfo> {
218 self.entities.iter().find(|entity| entity.name == name)
219 }
220
221 fn remember(&mut self, entity: &str, key: &Value) -> Result<(), SpaceError> {
222 let Some(text) = key_text(key) else { return Ok(()) };
223 let slot = (entity.to_owned(), text);
224 if !self.before.contains_key(&slot) {
225 let found = self.inner.find(entity, key)?;
226 self.before.insert(slot, found);
227 }
228 Ok(())
229 }
230
231 fn remember_object(&mut self, object: &Object) -> Result<(), SpaceError> {
232 let key = self
233 .entity(object.entity())
234 .and_then(|entity| entity.key_field())
235 .map(|field| object.get(&field.name));
236 match key {
237 Some(key) => self.remember(object.entity(), &key),
238 None => Ok(()),
239 }
240 }
241
242 fn collections_of(&self, entity: &str, reference: &FieldInfo) -> Vec<(&EntityInfo, &FieldInfo)> {
244 let FieldKind::Reference(owner) = &reference.kind else { return Vec::new() };
245 let Some(owner) = self.entity(owner) else { return Vec::new() };
246 owner
247 .fields
248 .iter()
249 .filter(|field| matches!(&field.kind, FieldKind::Collection(target) if target == entity))
250 .filter(|field| field.back_reference.as_ref().is_none_or(|back| *back == reference.name))
251 .map(|field| (owner, field))
252 .collect()
253 }
254
255 fn collection_lines(
257 &self,
258 entity: &EntityInfo,
259 key: &str,
260 old: Option<&Object>,
261 new: Option<&Object>,
262 lines: &mut Vec<Line>,
263 ) {
264 let settings = &self.trail.settings;
265 for reference in entity.fields.iter().filter(|field| matches!(field.kind, FieldKind::Reference(_))) {
266 let was = old.map_or(Value::Null, |object| object.get(&reference.name));
267 let is = new.map_or(Value::Null, |object| object.get(&reference.name));
268 for (owner, collection) in self.collections_of(&entity.name, reference) {
269 if settings.mode_of(&owner.name) != AuditMode::Full {
270 continue;
271 }
272 let mut note = |change, on: &Value| {
273 if let Some(text) = key_text(on) {
274 lines.push(
275 Line::of(change, &owner.name, &text)
276 .field(&collection.name)
277 .values(Value::Null, Value::Text(format!("{} {key}", entity.name))),
278 );
279 }
280 };
281 if was != is {
282 note("removed_from_collection", &was);
283 note("added_to_collection", &is);
284 } else if let (Some(old), Some(new)) = (old, new)
285 && entity
286 .fields
287 .iter()
288 .any(|f| settings.covers(f, &entity.name) && old.get(&f.name) != new.get(&f.name))
289 {
290 note("collection_object_changed", &is);
291 }
292 }
293 }
294 }
295
296 fn lines_of(&self, changes: &[Change]) -> Vec<Line> {
297 let settings = &self.trail.settings;
298 let mut lines = Vec::new();
299 for change in changes {
300 let (name, key_value, object) = match change {
301 Change::New(object) | Change::Updated(object) => {
302 let key = self
303 .entity(object.entity())
304 .and_then(EntityInfo::key_field)
305 .map_or(Value::Null, |field| object.get(&field.name));
306 (object.entity().to_owned(), key, Some(object))
307 }
308 Change::Deleted { entity, key } => (entity.clone(), key.clone(), None),
309 };
310 let Some(entity) = self.entity(&name) else { continue };
311 let mode = settings.mode_of(&name);
312 let key = key_text(&key_value).unwrap_or_default();
313 let old = self
314 .before
315 .get(&(name.clone(), key.clone()))
316 .and_then(Option::as_ref)
317 .filter(|_| !matches!(change, Change::New(_)));
318 match (mode, change) {
319 (AuditMode::Off, _) => {}
320 (_, Change::New(_)) => lines.push(Line::of("created", &name, &key)),
321 (AuditMode::CreationOnly, _) => {}
322 (AuditMode::Lightweight, Change::Updated(_)) => lines.push(Line::of("changed", &name, &key)),
323 (_, Change::Deleted { .. }) => lines.push(Line::of("deleted", &name, &key)),
324 _ => {}
325 }
326 if mode != AuditMode::Full {
327 continue;
328 }
329 if let Some(object) = object {
330 let fresh = entity.new_object();
331 for field in entity.fields.iter().filter(|field| settings.covers(field, &name)) {
332 let new = object.get(&field.name);
333 match (change, old) {
334 (Change::New(_), _) => {
335 if !field.is_key && new != fresh.get(&field.name) {
336 lines.push(
337 Line::of("initial_value", &name, &key)
338 .field(&field.name)
339 .values(Value::Null, shown(field, &new)),
340 );
341 }
342 }
343 (_, Some(old)) if old.get(&field.name) != new => lines.push(
344 Line::of("changed", &name, &key)
345 .field(&field.name)
346 .values(shown(field, &old.get(&field.name)), shown(field, &new)),
347 ),
348 _ => {}
349 }
350 }
351 }
352 self.collection_lines(entity, &key, old, object, &mut lines);
353 }
354 lines
355 }
356}
357
358#[cfg_attr(feature = "live", ravenrs_live::expose(part = "objectspace"))]
359impl ObjectSpace for AuditedObjectSpace {
360 fn entities(&self) -> Vec<EntityInfo> {
361 self.inner.entities()
362 }
363
364 fn query(&self, entity: &str, query: &Query) -> Result<Vec<Object>, SpaceError> {
365 self.inner.query(entity, query)
366 }
367
368 fn find(&self, entity: &str, key: &Value) -> Result<Option<Object>, SpaceError> {
369 self.inner.find(entity, key)
370 }
371
372 fn new_object(&mut self, entity: &str) -> Result<Object, SpaceError> {
373 self.inner.new_object(entity)
374 }
375
376 fn save(&mut self, object: Object) -> Result<(), SpaceError> {
377 self.remember_object(&object)?;
378 self.inner.save(object)
379 }
380
381 fn insert(&mut self, object: Object) -> Result<(), SpaceError> {
382 self.remember_object(&object)?;
383 self.inner.insert(object)
384 }
385
386 fn delete(&mut self, entity: &str, key: &Value) -> Result<(), SpaceError> {
387 self.remember(entity, key)?;
388 self.inner.delete(entity, key)
389 }
390
391 fn changes(&self) -> Vec<Change> {
392 self.inner.changes()
393 }
394
395 fn commit(&mut self) -> Result<(), SpaceError> {
396 let changes = self.inner.changes();
397 let mut lines = self.lines_of(&changes);
398 let noted: Vec<Line> = self.trail.waiting.borrow().clone();
399 lines.extend(noted.iter().cloned());
400 let written = self.trail.write(&mut *self.inner, &lines)?;
401 match self.inner.commit() {
402 Ok(()) => {
403 self.trail.waiting.borrow_mut().drain(..noted.len());
404 self.before.clear();
405 Ok(())
406 }
407 Err(error) => {
408 for key in written {
410 self.inner.delete(SECURITY_AUDIT_ENTRY, &key)?;
411 }
412 Err(error)
413 }
414 }
415 }
416
417 fn rollback(&mut self) {
418 self.before.clear();
419 self.inner.rollback();
420 }
421
422 fn refresh(&mut self) {
423 self.before.clear();
424 self.inner.refresh();
425 }
426
427 fn on_event(&mut self, watcher: SpaceWatcher) {
428 self.inner.on_event(watcher);
429 }
430}