kanidmd_lib/plugins/
namehistory.rs

1use std::sync::Arc;
2
3use crate::entry::{EntryInvalidCommitted, EntrySealedCommitted};
4use crate::event::ModifyEvent;
5use crate::plugins::Plugin;
6use crate::prelude::*;
7use crate::prelude::{BatchModifyEvent, QueryServerWriteTransaction};
8use crate::repl::cid::Cid;
9use crate::value::PartialValue;
10
11pub struct NameHistory {}
12
13lazy_static! {
14    // it contains all the partialvalues used to match against an Entry's class,
15    // we just need a partialvalue to match in order to target the entry
16    static ref CLASS_TO_UPDATE: PartialValue = PartialValue::new_iutf8(EntryClass::Account.into());
17}
18
19impl NameHistory {
20    fn is_entry_to_update<VALUE, STATE>(entry: &mut Entry<VALUE, STATE>) -> bool {
21        entry.attribute_equality(Attribute::Class, &CLASS_TO_UPDATE)
22    }
23
24    fn handle_name_updates(
25        pre_cand: &[Arc<EntrySealedCommitted>],
26        cand: &mut Vec<EntryInvalidCommitted>,
27        cid: &Cid,
28    ) -> Result<(), OperationError> {
29        for (pre, post) in pre_cand.iter().zip(cand) {
30            // here we check if the current entry has at least one of the classes we intend to target
31            if Self::is_entry_to_update(post) {
32                let pre_name_option = pre.get_ava_single(Attribute::Name);
33                let post_name_option = post.get_ava_single(Attribute::Name);
34                if let (Some(pre_name), Some(post_name)) = (pre_name_option, post_name_option) {
35                    if pre_name != post_name {
36                        match post_name {
37                            Value::Iname(n) => post.add_ava_if_not_exist(
38                                Attribute::NameHistory,
39                                Value::AuditLogString(cid.clone(), n),
40                            ),
41                            _ => return Err(OperationError::InvalidValueState),
42                        }
43                    }
44                }
45            }
46        }
47        Ok(())
48    }
49
50    fn handle_name_creation(
51        cands: &mut [EntryInvalidNew],
52        cid: &Cid,
53    ) -> Result<(), OperationError> {
54        for cand in cands.iter_mut() {
55            if Self::is_entry_to_update(cand) {
56                if let Some(name) = cand.get_ava_single(Attribute::Name) {
57                    match name {
58                        Value::Iname(n) => cand.add_ava_if_not_exist(
59                            Attribute::NameHistory,
60                            Value::AuditLogString(cid.clone(), n),
61                        ),
62                        _ => return Err(OperationError::InvalidValueState),
63                    }
64                }
65            }
66        }
67
68        Ok(())
69    }
70}
71
72impl Plugin for NameHistory {
73    fn id() -> &'static str {
74        "plugin_name_history"
75    }
76
77    #[instrument(level = "debug", name = "name_history::pre_create_transform", skip_all)]
78    fn pre_create_transform(
79        qs: &mut QueryServerWriteTransaction,
80        cand: &mut Vec<EntryInvalidNew>,
81        _ce: &CreateEvent,
82    ) -> Result<(), OperationError> {
83        Self::handle_name_creation(cand, qs.get_txn_cid())
84    }
85
86    #[instrument(level = "debug", name = "name_history::pre_modify", skip_all)]
87    fn pre_modify(
88        qs: &mut QueryServerWriteTransaction,
89        pre_cand: &[Arc<EntrySealedCommitted>],
90        cand: &mut Vec<EntryInvalidCommitted>,
91        _me: &ModifyEvent,
92    ) -> Result<(), OperationError> {
93        Self::handle_name_updates(pre_cand, cand, qs.get_txn_cid())
94    }
95
96    #[instrument(level = "debug", name = "name_history::pre_batch_modify", skip_all)]
97    fn pre_batch_modify(
98        qs: &mut QueryServerWriteTransaction,
99        pre_cand: &[Arc<EntrySealedCommitted>],
100        cand: &mut Vec<EntryInvalidCommitted>,
101        _me: &BatchModifyEvent,
102    ) -> Result<(), OperationError> {
103        Self::handle_name_updates(pre_cand, cand, qs.get_txn_cid())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use std::time::Duration;
110
111    use crate::entry::{Entry, EntryInit, EntryNew};
112    use crate::prelude::entries::Attribute;
113    use crate::prelude::{uuid, EntryClass};
114    use crate::repl::cid::Cid;
115    use crate::value::Value;
116    use crate::valueset::AUDIT_LOG_STRING_CAPACITY;
117
118    #[test]
119    fn name_purge_and_set() {
120        // Add another uuid to a type
121        let cid = Cid::new(
122            uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"),
123            Duration::new(20, 2),
124        );
125        let ea = entry_init!(
126            (Attribute::Class, EntryClass::Account.to_value()),
127            (Attribute::Class, EntryClass::PosixAccount.to_value()),
128            (Attribute::Name, Value::new_iname("old_name")),
129            (
130                Attribute::Uuid,
131                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
132            ),
133            (
134                Attribute::NameHistory,
135                Value::new_audit_log_string((cid.clone(), "old_name".to_string())).unwrap()
136            ),
137            (Attribute::Description, Value::new_utf8s("testperson")),
138            (Attribute::DisplayName, Value::new_utf8s("old name person"))
139        );
140        let preload = vec![ea];
141        run_modify_test!(
142            Ok(()),
143            preload,
144            filter!(f_eq(Attribute::Name, PartialValue::new_iname("old_name"))),
145            modlist!([
146                m_purge(Attribute::Name),
147                m_pres(Attribute::Name, &Value::new_iname("new_name_1"))
148            ]),
149            None,
150            |_| {},
151            |qs: &mut QueryServerWriteTransaction| {
152                let e = qs
153                    .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
154                    .expect("failed to get entry");
155                let c = e
156                    .get_ava_set(Attribute::NameHistory)
157                    .expect("failed to get primary cred.");
158                trace!("{:?}", c.clone());
159                assert!(
160                    c.contains(&PartialValue::new_utf8s("old_name"))
161                        && c.contains(&PartialValue::new_utf8s("new_name_1"))
162                )
163            }
164        );
165    }
166
167    #[test]
168    fn name_creation() {
169        // Add another uuid to a type
170        let ea = entry_init!(
171            (Attribute::Class, EntryClass::Account.to_value()),
172            (Attribute::Class, EntryClass::PosixAccount.to_value()),
173            (Attribute::Name, Value::new_iname("old_name")),
174            (
175                Attribute::Uuid,
176                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47e1"))
177            ),
178            (Attribute::Description, Value::new_utf8s("testperson")),
179            (Attribute::DisplayName, Value::new_utf8s("old name person"))
180        );
181        let preload = Vec::with_capacity(0);
182        let create = vec![ea];
183        run_create_test!(
184            Ok(()),
185            preload,
186            create,
187            None,
188            |qs: &mut QueryServerWriteTransaction| {
189                let e = qs
190                    .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47e1"))
191                    .expect("failed to get entry");
192                trace!("{:?}", e.get_ava());
193                let name_history = e
194                    .get_ava_set(Attribute::NameHistory)
195                    .expect("failed to get name_history ava");
196
197                assert!(name_history.contains(&PartialValue::new_utf8s("old_name")))
198            }
199        );
200    }
201
202    #[test]
203    fn name_purge_and_set_with_filled_history() {
204        let mut cids: Vec<Cid> = Vec::with_capacity(0);
205        for i in 1..AUDIT_LOG_STRING_CAPACITY {
206            cids.push(Cid::new(
207                uuid!("d2b496bd-8493-47b7-8142-f568b5cf47e1"),
208                Duration::new(20 + i as u64, 0),
209            ))
210        }
211        // Add another uuid to a type
212        let mut ea = entry_init!(
213            (Attribute::Class, EntryClass::Account.to_value()),
214            (Attribute::Class, EntryClass::PosixAccount.to_value()),
215            (Attribute::Name, Value::new_iname("old_name8")),
216            (
217                Attribute::Uuid,
218                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
219            ),
220            (Attribute::Description, Value::new_utf8s("testperson")),
221            (Attribute::DisplayName, Value::new_utf8s("old name person"))
222        );
223        for (i, cid) in cids.iter().enumerate() {
224            let index = 1 + i;
225            let name = format!("old_name{index}");
226            ea.add_ava(
227                Attribute::NameHistory,
228                Value::AuditLogString(cid.clone(), name),
229            )
230        }
231        let preload = vec![ea];
232        run_modify_test!(
233            Ok(()),
234            preload,
235            filter!(f_eq(Attribute::Name, PartialValue::new_iname("old_name8"))),
236            modlist!([
237                m_purge(Attribute::Name),
238                m_pres(Attribute::Name, &Value::new_iname("new_name"))
239            ]),
240            None,
241            |_| {},
242            |qs: &mut QueryServerWriteTransaction| {
243                let e = qs
244                    .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
245                    .expect("failed to get entry");
246                let c = e
247                    .get_ava_set(Attribute::NameHistory)
248                    .expect("failed to get name_history ava :/");
249                trace!(?c);
250                assert!(
251                    !c.contains(&PartialValue::new_utf8s("old_name1"))
252                        && c.contains(&PartialValue::new_utf8s("new_name"))
253                )
254            }
255        );
256    }
257}