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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! 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()
}