1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! Field level data audit: which record a call saved, which fields changed and how.
//!
//! The server collects this in a [`Trail`] on the request context while a call runs, and
//! `AuditLog` writes it with the call's audit record. Old values always come from the record the
//! server loaded, never from the client.
use crate::model::Model;
use crate::value::Value;
use serde::Serialize;
/// One changed field. Values are plain strings; masked fields stay masked.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct FieldChange {
pub field: &'static str,
pub old: Option<String>,
pub new: Option<String>,
}
/// A record named by its model and key. `id` is `None` for models without a key.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RecordRef {
pub model: &'static str,
pub id: Option<String>,
}
impl RecordRef {
/// The record `value` stands for.
pub fn of<M: Model>(value: &M) -> Self {
let id = M::fields().iter().position(|f| f.key).and_then(|i| value.value_at(i)).and_then(|v| plain(&v));
RecordRef { model: M::NAME, id }
}
}
/// An export of a list of sensitive records: one record per file, with the number of rows and
/// the sort and filters the user asked for, instead of one read per row.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ExportRecord {
pub model: &'static str,
/// `csv`, `xlsx` or `pdf`.
pub format: &'static str,
pub rows: u64,
/// `{ "sort": [...], "filters": [...] }` as asked.
pub query: serde_json::Value,
}
/// One field of one record, such as a revealed masked value.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct FieldRef {
pub model: &'static str,
pub id: Option<String>,
pub field: &'static str,
}
/// What a call did to data. Collected on the request context; read by audit interceptors.
#[derive(Clone, Debug, Default, Serialize)]
pub struct Trail {
/// The record the call saved.
pub record: Option<RecordRef>,
/// Fields that changed, in declaration order.
pub changes: Vec<FieldChange>,
/// Masked values the user was shown in full.
pub revealed: Vec<FieldRef>,
/// Sensitive records the user was shown. For screens marked `sensitive` the model is the
/// screen name and the id its params.
pub reads: Vec<RecordRef>,
/// Why the user opened a screen that asks for a reason.
pub reason: Option<String>,
/// A workflow move made by the call.
pub transition: Option<crate::workflow::TransitionRecord>,
/// An export of sensitive records made by the call.
pub export: Option<ExportRecord>,
/// Reason the user gave, with the screen it was given for. Checked by the server.
#[serde(skip)]
pub(crate) given_reason: Option<(String, String)>,
}
/// A value as a plain, language independent string. `None` for an empty value.
///
/// ```
/// use zeyn::{Money, Value};
/// assert_eq!(zeyn::audit::plain(&Value::Money(Money::tl(123456))).as_deref(), Some("1234.56 TRY"));
/// assert_eq!(zeyn::audit::plain(&Value::Null), None);
/// ```
pub fn plain(value: &Value) -> Option<String> {
Some(match value {
Value::Null => return None,
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Decimal(d) => d.to_string(),
Value::Number(d) => d.to_string(),
Value::Text(s) => s.clone(),
Value::Money(m) => {
let sign = if m.minor < 0 { "-" } else { "" };
let abs = m.minor.unsigned_abs();
format!("{sign}{}.{:02} {}", abs / 100, abs % 100, format!("{:?}", m.currency).to_uppercase())
}
Value::Date(d) => d.to_string(),
Value::Time(t) => t.to_string(),
Value::DateTime(d) => d.to_string(),
Value::LocalDateTime(l) => l.to_string(),
Value::Choice(c) => c.to_string(),
})
}
/// Fields whose value differs between the loaded record and the saved one. Row versions are left
/// out; masked fields are reported masked.
pub fn diff<M: Model>(old: &M, new: &M) -> Vec<FieldChange> {
M::fields()
.iter()
.enumerate()
.filter(|(_, meta)| !meta.version)
.filter_map(|(i, meta)| {
let (a, b) = (old.value_at(i).unwrap_or(Value::Null), new.value_at(i).unwrap_or(Value::Null));
if a == b {
return None;
}
let shown = |v: &Value| plain(v).map(|s| crate::privacy::masked_text(meta, &s));
Some(FieldChange { field: meta.name, old: shown(&a), new: shown(&b) })
})
.collect()
}