kanidmd_lib/valueset/
auditlogstring.rs

1use crate::prelude::*;
2use crate::repl::cid::Cid;
3use crate::schema::SchemaAttribute;
4use crate::valueset::ScimResolveStatus;
5use crate::valueset::{DbValueSetV2, ValueSet};
6use kanidm_proto::scim_v1::server::ScimAuditString;
7use std::collections::BTreeMap;
8use time::OffsetDateTime;
9
10type AuditLogStringType = (Cid, String);
11
12pub const AUDIT_LOG_STRING_CAPACITY: usize = 9;
13
14#[derive(Debug, Clone)]
15pub struct ValueSetAuditLogString {
16    map: BTreeMap<Cid, String>,
17}
18
19impl ValueSetAuditLogString {
20    fn remove_oldest(&mut self) {
21        // pop to size.
22        while self.map.len() > AUDIT_LOG_STRING_CAPACITY {
23            self.map.pop_first();
24        }
25    }
26
27    pub fn new((c, s): AuditLogStringType) -> Box<Self> {
28        let mut map = BTreeMap::new();
29        map.insert(c, s);
30        Box::new(ValueSetAuditLogString { map })
31    }
32
33    pub fn from_dbvs2(data: Vec<AuditLogStringType>) -> Result<ValueSet, OperationError> {
34        let map = data.into_iter().collect();
35        Ok(Box::new(ValueSetAuditLogString { map }))
36    }
37}
38
39impl ValueSetT for ValueSetAuditLogString {
40    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
41        match value {
42            Value::AuditLogString(c, s) => {
43                let r = self.map.insert(c, s);
44                self.remove_oldest();
45                // true if insert was a new value.
46                Ok(r.is_none())
47            }
48            _ => {
49                debug_assert!(false);
50                Err(OperationError::InvalidValueState)
51            }
52        }
53    }
54
55    fn clear(&mut self) {
56        self.map.clear();
57    }
58
59    fn remove(&mut self, _pv: &PartialValue, _cid: &Cid) -> bool {
60        false
61    }
62
63    fn contains(&self, pv: &PartialValue) -> bool {
64        match pv {
65            PartialValue::Utf8(s) => self.map.values().any(|current| s.eq(current)),
66            PartialValue::Cid(c) => self.map.contains_key(c),
67            _ => {
68                debug_assert!(false);
69                true
70            }
71        }
72    }
73
74    fn substring(&self, _pv: &PartialValue) -> bool {
75        false
76    }
77
78    fn startswith(&self, _pv: &PartialValue) -> bool {
79        false
80    }
81
82    fn endswith(&self, _pv: &PartialValue) -> bool {
83        false
84    }
85
86    fn lessthan(&self, _pv: &PartialValue) -> bool {
87        false
88    }
89
90    fn len(&self) -> usize {
91        self.map.len()
92    }
93
94    fn generate_idx_eq_keys(&self) -> Vec<String> {
95        self.map.iter().map(|(d, s)| format!("{d}-{s}")).collect()
96    }
97
98    fn syntax(&self) -> SyntaxType {
99        SyntaxType::AuditLogString
100    }
101
102    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
103        self.map
104            .iter()
105            .all(|(_, s)| Value::validate_str_escapes(s) && Value::validate_singleline(s))
106            && self.map.len() <= AUDIT_LOG_STRING_CAPACITY
107    }
108
109    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
110        Box::new(self.map.iter().map(|(d, s)| format!("{d}-{s}")))
111    }
112
113    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
114        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
115            self.map
116                .iter()
117                .map(|(cid, strdata)| {
118                    let odt: OffsetDateTime = cid.into();
119                    ScimAuditString {
120                        date_time: odt,
121                        value: strdata.clone(),
122                    }
123                })
124                .collect::<Vec<_>>(),
125        )))
126    }
127
128    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
129        DbValueSetV2::AuditLogString(
130            self.map
131                .iter()
132                .map(|(c, s)| (c.clone(), s.clone()))
133                .collect(),
134        )
135    }
136
137    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
138        Box::new(self.map.keys().map(|c| PartialValue::Cid(c.clone())))
139    }
140
141    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
142        Box::new(
143            self.map
144                .iter()
145                .map(|(c, s)| Value::AuditLogString(c.clone(), s.clone())),
146        )
147    }
148
149    fn equal(&self, other: &ValueSet) -> bool {
150        if let Some(other) = other.as_audit_log_string() {
151            &self.map == other
152        } else {
153            debug_assert!(false);
154            false
155        }
156    }
157
158    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
159        if let Some(b) = other.as_audit_log_string() {
160            mergemaps!(self.map, b)?;
161            self.remove_oldest();
162            Ok(())
163        } else {
164            debug_assert!(false);
165            Err(OperationError::InvalidValueState)
166        }
167    }
168
169    #[allow(clippy::todo)]
170    fn repl_merge_valueset(&self, older: &ValueSet, _trim_cid: &Cid) -> Option<ValueSet> {
171        if let Some(mut map) = older.as_audit_log_string().cloned() {
172            // Merge maps is right-preferencing, so this means that
173            // newer content always wins over.
174            mergemaps!(map, self.map)
175                .map_err(|_: OperationError| ())
176                .ok()?;
177            let mut new_vs = Box::new(ValueSetAuditLogString { map });
178            new_vs.remove_oldest();
179            Some(new_vs)
180        } else {
181            debug_assert!(false);
182            None
183        }
184    }
185
186    fn as_audit_log_string(&self) -> Option<&BTreeMap<Cid, String>> {
187        Some(&self.map)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
194    use crate::repl::cid::Cid;
195    use crate::value::Value;
196    use crate::valueset::ValueSet;
197    use std::time::Duration;
198
199    #[test]
200    fn test_valueset_auditlogstring_merge() {
201        let mut vs: ValueSet = ValueSetAuditLogString::new((Cid::new_count(0), "A".to_string()));
202        assert_eq!(vs.len(), 1);
203
204        for i in 1..AUDIT_LOG_STRING_CAPACITY {
205            vs.insert_checked(Value::AuditLogString(
206                Cid::new_count(i as u64),
207                "A".to_string(),
208            ))
209            .unwrap();
210        }
211
212        assert_eq!(vs.len(), AUDIT_LOG_STRING_CAPACITY);
213
214        // Add one extra
215        vs.insert_checked(Value::AuditLogString(
216            Cid::new_count(AUDIT_LOG_STRING_CAPACITY as u64),
217            "A".to_string(),
218        ))
219        .unwrap();
220
221        assert_eq!(vs.len(), AUDIT_LOG_STRING_CAPACITY);
222
223        let mut v_iter = vs.to_value_iter();
224        let Some(Value::AuditLogString(c, _s)) = v_iter.next() else {
225            unreachable!();
226        };
227        // Should always be '1' since the set merge would have pushed '0' (ring-buffer);
228        assert_eq!(c.ts, Duration::from_secs(1));
229        println!("{:?}", c);
230        drop(v_iter);
231
232        // Make a second set.
233        let other_vs: ValueSet = ValueSetAuditLogString::new(
234            // Notice that 0 here is older than our other set items.
235            (Cid::new_count(0), "A".to_string()),
236        );
237        assert_eq!(other_vs.len(), 1);
238
239        // Merge. The content of other_vs should be dropped.
240        vs.merge(&other_vs)
241            .expect("Failed to merge, incorrect types");
242
243        // No change in the state of the set.
244        assert_eq!(vs.len(), AUDIT_LOG_STRING_CAPACITY);
245        let mut v_iter = vs.to_value_iter();
246        let Some(Value::AuditLogString(c, _s)) = v_iter.next() else {
247            unreachable!();
248        };
249        // Should always be '1' since the set merge would have pushed '0' (ring-buffer);
250        assert_eq!(c.ts, Duration::from_secs(1));
251        println!("{:?}", c);
252        drop(v_iter);
253
254        // Now merge in with a set that has a value that is newer.
255
256        #[allow(clippy::bool_assert_comparison, clippy::assertions_on_constants)]
257        {
258            assert!(100 > AUDIT_LOG_STRING_CAPACITY);
259        }
260
261        let other_vs: ValueSet = ValueSetAuditLogString::new(
262            // Notice that 0 here is older than our other set items.
263            (Cid::new_count(100), "A".to_string()),
264        );
265        assert_eq!(other_vs.len(), 1);
266
267        vs.merge(&other_vs)
268            .expect("Failed to merge, incorrect types");
269
270        // New value has pushed out the next oldest.
271        assert_eq!(vs.len(), AUDIT_LOG_STRING_CAPACITY);
272        let mut v_iter = vs.to_value_iter();
273        let Some(Value::AuditLogString(c, _s)) = v_iter.next() else {
274            unreachable!();
275        };
276        // Should always be '1' since the set merge would have pushed '0' (ring-buffer);
277        println!("{:?}", c);
278        assert_eq!(c.ts, Duration::from_secs(2));
279        drop(v_iter);
280    }
281
282    #[test]
283    fn test_valueset_auditlogstring_repl_merge() {
284        let zero_cid = Cid::new_zero();
285        let mut vs: ValueSet = ValueSetAuditLogString::new((Cid::new_count(1), "A".to_string()));
286        assert_eq!(vs.len(), 1);
287
288        for i in 2..(AUDIT_LOG_STRING_CAPACITY + 1) {
289            vs.insert_checked(Value::AuditLogString(
290                Cid::new_count(i as u64),
291                "A".to_string(),
292            ))
293            .unwrap();
294        }
295
296        assert_eq!(vs.len(), AUDIT_LOG_STRING_CAPACITY);
297
298        // Make a second set.
299        let other_vs: ValueSet = ValueSetAuditLogString::new(
300            // Notice that 0 here is older than our other set items.
301            (Cid::new_count(0), "A".to_string()),
302        );
303        assert_eq!(other_vs.len(), 1);
304
305        // Merge. The content of other_vs should be dropped.
306        let r_vs = vs
307            .repl_merge_valueset(&other_vs, &zero_cid)
308            .expect("merge did not occur");
309
310        // No change in the state of the set.
311        assert_eq!(r_vs.len(), AUDIT_LOG_STRING_CAPACITY);
312        let mut v_iter = r_vs.to_value_iter();
313        let Some(Value::AuditLogString(c, _s)) = v_iter.next() else {
314            unreachable!();
315        };
316        // Should always be '1' since the set merge would have pushed '0' (ring-buffer);
317        assert_eq!(c.ts, Duration::from_secs(1));
318        println!("{:?}", c);
319        drop(v_iter);
320
321        // Now merge in with a set that has a value that is newer.
322
323        #[allow(clippy::bool_assert_comparison, clippy::assertions_on_constants)]
324        {
325            assert!(100 > AUDIT_LOG_STRING_CAPACITY);
326        }
327
328        let other_vs: ValueSet = ValueSetAuditLogString::new(
329            // Notice that 0 here is older than our other set items.
330            (Cid::new_count(100), "A".to_string()),
331        );
332        assert_eq!(other_vs.len(), 1);
333
334        let r_vs = vs
335            .repl_merge_valueset(&other_vs, &zero_cid)
336            .expect("merge did not occur");
337
338        // New value has pushed out the next oldest.
339        assert_eq!(r_vs.len(), AUDIT_LOG_STRING_CAPACITY);
340        let mut v_iter = r_vs.to_value_iter();
341        let Some(Value::AuditLogString(c, _s)) = v_iter.next() else {
342            unreachable!();
343        };
344        // Should always be '1' since the set merge would have pushed '0' (ring-buffer);
345        println!("{:?}", c);
346        assert_eq!(c.ts, Duration::from_secs(2));
347        drop(v_iter);
348    }
349
350    #[test]
351    fn test_scim_auditlog_string() {
352        let mut vs: ValueSet = ValueSetAuditLogString::new((Cid::new_count(0), "A".to_string()));
353        assert!(vs.len() == 1);
354
355        for i in 1..AUDIT_LOG_STRING_CAPACITY {
356            vs.insert_checked(Value::AuditLogString(
357                Cid::new_count(i as u64),
358                "A".to_string(),
359            ))
360            .unwrap();
361        }
362
363        let data = r#"
364[
365  {
366    "dateTime": "1970-01-01T00:00:00Z",
367    "value": "A"
368  },
369  {
370    "dateTime": "1970-01-01T00:00:01Z",
371    "value": "A"
372  },
373  {
374    "dateTime": "1970-01-01T00:00:02Z",
375    "value": "A"
376  },
377  {
378    "dateTime": "1970-01-01T00:00:03Z",
379    "value": "A"
380  },
381  {
382    "dateTime": "1970-01-01T00:00:04Z",
383    "value": "A"
384  },
385  {
386    "dateTime": "1970-01-01T00:00:05Z",
387    "value": "A"
388  },
389  {
390    "dateTime": "1970-01-01T00:00:06Z",
391    "value": "A"
392  },
393  {
394    "dateTime": "1970-01-01T00:00:07Z",
395    "value": "A"
396  },
397  {
398    "dateTime": "1970-01-01T00:00:08Z",
399    "value": "A"
400  }
401]
402"#;
403        crate::valueset::scim_json_reflexive(&vs, data);
404    }
405}