Skip to main content

ravenrs_app/security/
audit.rs

1//! A record of who changed what, written in the same commit as the change.
2
3use 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/// How much is written about the records of an entity.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15#[non_exhaustive]
16#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
17pub enum AuditMode {
18    /// Everything, with the fields and their values, before and after.
19    #[default]
20    Full,
21    /// That a record was created, changed or deleted, with no fields or values.
22    Lightweight,
23    /// Only that a record was created, as when bringing in a lot of records.
24    CreationOnly,
25    /// Nothing.
26    Off,
27}
28
29/// What the audit trail covers.
30#[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    /// Everything, in this mode.
41    pub fn new(mode: AuditMode) -> Self {
42        Self { mode, ..Self::default() }
43    }
44
45    /// This entity in another mode.
46    pub fn entity(mut self, entity: impl Into<String>, mode: AuditMode) -> Self {
47        self.modes.insert(entity.into(), mode);
48        self
49    }
50
51    /// Leaves this field out.
52    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/// One line of the trail, before it is written.
72#[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/// What the audit trail needs: what it covers, who is acting, and what to add to the next commit.
108#[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    /// Writes what was noted outside a commit, as nothing else is on the way for it.
114    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    /// With these settings; `author` says who is acting.
126    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    /// Writes notes there right away, instead of with the next commit.
131    pub fn write_with(&self, space: SpaceMaker) {
132        *self.writer.borrow_mut() = Some(space);
133    }
134
135    /// Adds something of the application's to the trail, about this record and field. It is
136    /// written right away with [`write_with`](Self::write_with), or else with the next commit.
137    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    /// Adds these lines to what waits in this space, and says what it added.
152    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
173/// A value as the trail shows it. What is too big to read is only said to be there.
174fn 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/// A unit of work that writes what it commits to the audit trail, in the same commit, so a change
193/// and its trail stay or go together. Changes made to the database some other way are not seen.
194#[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    /// Each record as it was before this unit of work first touched it.
200    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    /// Over this, writing to this trail.
212    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    /// The collections that list records of this entity through this reference.
243    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    /// What this change says about the lists that hold the record, for the records that own them.
256    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                // What was added goes away, so trying again does not write it twice.
409                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}