1use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntrySealed};
4use crate::event::{CreateEvent, ModifyEvent};
5use crate::plugins::Plugin;
6use crate::prelude::*;
7use std::collections::BTreeSet;
8use std::sync::Arc;
9
10pub struct Spn {}
11
12impl Plugin for Spn {
13    fn id() -> &'static str {
14        "plugin_spn"
15    }
16
17    #[instrument(level = "debug", name = "spn_pre_create_transform", skip_all)]
19    fn pre_create_transform(
20        qs: &mut QueryServerWriteTransaction,
21        cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
22        _ce: &CreateEvent,
23    ) -> Result<(), OperationError> {
24        Self::modify_inner(qs, cand)
28    }
29
30    #[instrument(level = "debug", name = "spn_pre_modify", skip_all)]
31    fn pre_modify(
32        qs: &mut QueryServerWriteTransaction,
33        _pre_cand: &[Arc<EntrySealedCommitted>],
34        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
35        _me: &ModifyEvent,
36    ) -> Result<(), OperationError> {
37        Self::modify_inner(qs, cand)
38    }
39
40    #[instrument(level = "debug", name = "spn_pre_batch_modify", skip_all)]
41    fn pre_batch_modify(
42        qs: &mut QueryServerWriteTransaction,
43        _pre_cand: &[Arc<EntrySealedCommitted>],
44        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
45        _me: &BatchModifyEvent,
46    ) -> Result<(), OperationError> {
47        Self::modify_inner(qs, cand)
48    }
49
50    #[instrument(level = "debug", name = "spn_post_modify", skip_all)]
51    fn post_modify(
52        qs: &mut QueryServerWriteTransaction,
53        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
55        cand: &[Entry<EntrySealed, EntryCommitted>],
56        _ce: &ModifyEvent,
57    ) -> Result<(), OperationError> {
58        Self::post_modify_inner(qs, pre_cand, cand)
59    }
60
61    #[instrument(level = "debug", name = "spn_post_batch_modify", skip_all)]
62    fn post_batch_modify(
63        qs: &mut QueryServerWriteTransaction,
64        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
66        cand: &[Entry<EntrySealed, EntryCommitted>],
67        _ce: &BatchModifyEvent,
68    ) -> Result<(), OperationError> {
69        Self::post_modify_inner(qs, pre_cand, cand)
70    }
71
72    #[instrument(level = "debug", name = "spn_post_repl_incremental", skip_all)]
73    fn post_repl_incremental(
74        qs: &mut QueryServerWriteTransaction,
75        pre_cand: &[Arc<EntrySealedCommitted>],
76        cand: &[EntrySealedCommitted],
77        _conflict_uuids: &BTreeSet<Uuid>,
78    ) -> Result<(), OperationError> {
79        Self::post_modify_inner(qs, pre_cand, cand)
80    }
81
82    #[instrument(level = "debug", name = "spn::verify", skip_all)]
83    fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
84        let domain_name = qs.get_domain_name().to_string();
90
91        let filt_in = filter!(f_or!([
92            f_eq(Attribute::Class, EntryClass::Group.into()),
93            f_eq(Attribute::Class, EntryClass::Account.into()),
94        ]));
95
96        let all_cand = match qs
97            .internal_search(filt_in)
98            .map_err(|_| Err(ConsistencyError::QueryServerSearchFailure))
99        {
100            Ok(all_cand) => all_cand,
101            Err(e) => return vec![e],
102        };
103
104        let mut r = Vec::with_capacity(0);
105
106        for e in all_cand {
107            let Some(g_spn) = e.generate_spn(&domain_name) else {
108                admin_error!(
109                    uuid = ?e.get_uuid(),
110                    "Entry SPN could not be generated (missing name!?)",
111                );
112                debug_assert!(false);
113                r.push(Err(ConsistencyError::InvalidSpn(e.get_id())));
114                continue;
115            };
116            match e.get_ava_set(Attribute::Spn) {
117                Some(r_spn) => {
118                    trace!("verify spn: s {:?} == ex {:?} ?", r_spn, g_spn);
119                    if r_spn != &g_spn {
120                        admin_error!(
121                            uuid = ?e.get_uuid(),
122                            "Entry SPN does not match expected s {:?} != ex {:?}",
123                            r_spn,
124                            g_spn,
125                        );
126                        debug_assert!(false);
127                        r.push(Err(ConsistencyError::InvalidSpn(e.get_id())))
128                    }
129                }
130                None => {
131                    admin_error!(uuid = ?e.get_uuid(), "Entry does not contain an SPN");
132                    r.push(Err(ConsistencyError::InvalidSpn(e.get_id())))
133                }
134            }
135        }
136        r
137    }
138}
139
140impl Spn {
141    fn modify_inner<T: Clone + std::fmt::Debug>(
142        qs: &mut QueryServerWriteTransaction,
143        cand: &mut [Entry<EntryInvalid, T>],
144    ) -> Result<(), OperationError> {
145        let domain_name = qs.get_domain_name();
146
147        for ent in cand.iter_mut() {
148            if ent.attribute_equality(Attribute::Class, &EntryClass::Group.into())
149                || ent.attribute_equality(Attribute::Class, &EntryClass::Account.into())
150            {
151                let spn_valueset = ent
152                    .generate_spn(domain_name)
153                    .ok_or(OperationError::InvalidEntryState)
154                    .map_err(|e| {
155                        error!(
156                            "Account or group missing name, unable to generate spn!? {:?} entry_id = {:?}",
157                            e, ent.get_uuid()
158                        );
159                        e
160                    })?;
161                trace!("plugin_spn: set spn to {:?}", spn_valueset);
162                ent.set_ava_set(&Attribute::Spn, spn_valueset);
163            }
164        }
165        Ok(())
166    }
167
168    fn post_modify_inner(
169        qs: &mut QueryServerWriteTransaction,
170        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
171        cand: &[Entry<EntrySealed, EntryCommitted>],
172    ) -> Result<(), OperationError> {
173        let domain_name_changed = cand.iter().zip(pre_cand.iter()).find_map(|(post, pre)| {
176            let domain_name = post.get_ava_single(Attribute::DomainName);
177            if post.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO)
178                && domain_name != pre.get_ava_single(Attribute::DomainName)
179            {
180                domain_name
181            } else {
182                None
183            }
184        });
185
186        let Some(domain_name) = domain_name_changed else {
187            return Ok(());
188        };
189
190        qs.reload_domain_info()?;
195
196        admin_info!(
197            "IMPORTANT!!! Changing domain name to \"{:?}\". THIS MAY TAKE A LONG TIME ...",
198            domain_name
199        );
200
201        qs.internal_modify(
204            &filter!(f_pres(Attribute::Spn)),
205            &modlist!([m_purge(Attribute::Spn)]),
206        )
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use crate::prelude::*;
213
214    #[test]
215    fn test_spn_generate_create() {
216        let e: Entry<EntryInit, EntryNew> = entry_init!(
218            (Attribute::Class, EntryClass::Account.to_value()),
219            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
220            (Attribute::Name, Value::new_iname("testperson")),
221            (Attribute::Description, Value::new_utf8s("testperson")),
222            (Attribute::DisplayName, Value::new_utf8s("testperson"))
223        );
224
225        let create = vec![e];
226        let preload = Vec::with_capacity(0);
227
228        run_create_test!(
229            Ok(None),
230            preload,
231            create,
232            None,
233            |_qs_write: &QueryServerWriteTransaction| {}
234        );
235        }
237
238    #[test]
239    fn test_spn_generate_modify() {
240        let e: Entry<EntryInit, EntryNew> = entry_init!(
242            (Attribute::Class, EntryClass::Account.to_value()),
243            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
244            (Attribute::Name, Value::new_iname("testperson")),
245            (Attribute::Description, Value::new_utf8s("testperson")),
246            (Attribute::DisplayName, Value::new_utf8s("testperson"))
247        );
248
249        let preload = vec![e];
250
251        run_modify_test!(
252            Ok(()),
253            preload,
254            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
255            modlist!([m_purge(Attribute::Spn)]),
256            None,
257            |_| {},
258            |_| {}
259        );
260    }
261
262    #[test]
263    fn test_spn_validate_create() {
264        let e: Entry<EntryInit, EntryNew> = entry_init!(
267            (Attribute::Class, EntryClass::Account.to_value()),
268            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
269            (
270                Attribute::Spn,
271                Value::new_utf8s("testperson@invalid_domain.com")
272            ),
273            (Attribute::Name, Value::new_iname("testperson")),
274            (Attribute::Description, Value::new_utf8s("testperson")),
275            (Attribute::DisplayName, Value::new_utf8s("testperson"))
276        );
277
278        let create = vec![e];
279        let preload = Vec::with_capacity(0);
280
281        run_create_test!(
282            Ok(None),
283            preload,
284            create,
285            None,
286            |_qs_write: &QueryServerWriteTransaction| {}
287        );
288    }
289
290    #[test]
291    fn test_spn_validate_modify() {
292        let e: Entry<EntryInit, EntryNew> = entry_init!(
295            (Attribute::Class, EntryClass::Account.to_value()),
296            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
297            (Attribute::Name, Value::new_iname("testperson")),
298            (Attribute::Description, Value::new_utf8s("testperson")),
299            (Attribute::DisplayName, Value::new_utf8s("testperson"))
300        );
301
302        let preload = vec![e];
303
304        run_modify_test!(
305            Ok(()),
306            preload,
307            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
308            modlist!([
309                m_purge(Attribute::Spn),
310                m_pres(
311                    Attribute::Spn,
312                    &Value::new_spn_str("invalid", Attribute::Spn.as_ref())
313                )
314            ]),
315            None,
316            |_| {},
317            |_| {}
318        );
319    }
320
321    #[qs_test]
322    async fn test_spn_regen_domain_rename(server: &QueryServer) {
323        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
324
325        let ex1 = Value::new_spn_str("a_testperson1", "example.com");
326        let ex2 = Value::new_spn_str("a_testperson1", "new.example.com");
327
328        let t_uuid = Uuid::new_v4();
329        let g_uuid = Uuid::new_v4();
330
331        assert!(server_txn
332            .internal_create(vec![
333                entry_init!(
334                    (Attribute::Class, EntryClass::Object.to_value()),
335                    (Attribute::Class, EntryClass::Account.to_value()),
336                    (Attribute::Class, EntryClass::Person.to_value()),
337                    (Attribute::Name, Value::new_iname("a_testperson1")),
338                    (Attribute::Uuid, Value::Uuid(t_uuid)),
339                    (Attribute::Description, Value::new_utf8s("testperson1")),
340                    (Attribute::DisplayName, Value::new_utf8s("testperson1"))
341                ),
342                entry_init!(
343                    (Attribute::Class, EntryClass::Object.to_value()),
344                    (Attribute::Class, EntryClass::Group.to_value()),
345                    (Attribute::Name, Value::new_iname("testgroup")),
346                    (Attribute::Uuid, Value::Uuid(g_uuid)),
347                    (Attribute::Member, Value::Refer(t_uuid))
348                ),
349            ])
350            .is_ok());
351
352        assert!(server_txn.commit().is_ok());
353        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
354
355        let e_pre = server_txn
358            .internal_search_uuid(t_uuid)
359            .expect("must not fail");
360
361        let e_pre_spn = e_pre.get_ava_single(Attribute::Spn).expect("must not fail");
362        assert_eq!(e_pre_spn, ex1);
363
364        server_txn
368            .danger_domain_rename("new.example.com")
369            .expect("should not fail!");
370
371        assert!(server_txn.commit().is_ok());
372        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
373
374        let e_post = server_txn
376            .internal_search_uuid(t_uuid)
377            .expect("must not fail");
378
379        let e_post_spn = e_post
380            .get_ava_single(Attribute::Spn)
381            .expect("must not fail");
382        debug!("{:?}", e_post_spn);
383        debug!("{:?}", ex2);
384        assert_eq!(e_post_spn, ex2);
385
386        let testuser_spn = server_txn
388            .uuid_to_spn(t_uuid)
389            .expect("Must be able to retrieve the spn")
390            .expect("Value must not be none");
391        assert_eq!(testuser_spn, ex2);
392
393        assert!(server_txn.commit().is_ok());
394    }
395}