kanidmd_lib/plugins/
spn.rs

1// Generate and manage spn's for all entries in the domain. Also deals with
2// the infrequent - but possible - case where a domain is renamed.
3use std::collections::BTreeSet;
4use std::iter::once;
5use std::sync::Arc;
6
7use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntrySealed};
8use crate::event::{CreateEvent, ModifyEvent};
9use crate::plugins::Plugin;
10use crate::prelude::*;
11
12pub struct Spn {}
13
14impl Plugin for Spn {
15    fn id() -> &'static str {
16        "plugin_spn"
17    }
18
19    // hook on pre-create and modify to generate / validate.
20    #[instrument(level = "debug", name = "spn_pre_create_transform", skip_all)]
21    fn pre_create_transform(
22        qs: &mut QueryServerWriteTransaction,
23        cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
24        _ce: &CreateEvent,
25    ) -> Result<(), OperationError> {
26        // Always generate the spn and set it. Why? Because the effort
27        // needed to validate is the same as generation, so we may as well
28        // just generate and set blindly when required.
29        Self::modify_inner(qs, cand)
30    }
31
32    #[instrument(level = "debug", name = "spn_pre_modify", skip_all)]
33    fn pre_modify(
34        qs: &mut QueryServerWriteTransaction,
35        _pre_cand: &[Arc<EntrySealedCommitted>],
36        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
37        _me: &ModifyEvent,
38    ) -> Result<(), OperationError> {
39        Self::modify_inner(qs, cand)
40    }
41
42    #[instrument(level = "debug", name = "spn_pre_batch_modify", skip_all)]
43    fn pre_batch_modify(
44        qs: &mut QueryServerWriteTransaction,
45        _pre_cand: &[Arc<EntrySealedCommitted>],
46        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
47        _me: &BatchModifyEvent,
48    ) -> Result<(), OperationError> {
49        Self::modify_inner(qs, cand)
50    }
51
52    #[instrument(level = "debug", name = "spn_post_modify", skip_all)]
53    fn post_modify(
54        qs: &mut QueryServerWriteTransaction,
55        // List of what we modified that was valid?
56        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
57        cand: &[Entry<EntrySealed, EntryCommitted>],
58        _ce: &ModifyEvent,
59    ) -> Result<(), OperationError> {
60        Self::post_modify_inner(qs, pre_cand, cand)
61    }
62
63    #[instrument(level = "debug", name = "spn_post_batch_modify", skip_all)]
64    fn post_batch_modify(
65        qs: &mut QueryServerWriteTransaction,
66        // List of what we modified that was valid?
67        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
68        cand: &[Entry<EntrySealed, EntryCommitted>],
69        _ce: &BatchModifyEvent,
70    ) -> Result<(), OperationError> {
71        Self::post_modify_inner(qs, pre_cand, cand)
72    }
73
74    #[instrument(level = "debug", name = "spn_post_repl_incremental", skip_all)]
75    fn post_repl_incremental(
76        qs: &mut QueryServerWriteTransaction,
77        pre_cand: &[Arc<EntrySealedCommitted>],
78        cand: &[EntrySealedCommitted],
79        _conflict_uuids: &BTreeSet<Uuid>,
80    ) -> Result<(), OperationError> {
81        Self::post_modify_inner(qs, pre_cand, cand)
82    }
83
84    #[instrument(level = "debug", name = "spn::verify", skip_all)]
85    fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
86        // Verify that all items with spn's have valid spns.
87        //   We need to consider the case that an item has a different origin domain too,
88        // so we should be able to verify that *those* spns validate to the trusted domain info
89        // we have been sent also. It's not up to use to generate those though ...
90
91        let domain_name = qs.get_domain_name().to_string();
92
93        let filt_in = filter!(f_or!([
94            f_eq(Attribute::Class, EntryClass::Group.into()),
95            f_eq(Attribute::Class, EntryClass::Account.into()),
96        ]));
97
98        let all_cand = match qs
99            .internal_search(filt_in)
100            .map_err(|_| Err(ConsistencyError::QueryServerSearchFailure))
101        {
102            Ok(all_cand) => all_cand,
103            Err(e) => return vec![e],
104        };
105
106        let mut r = Vec::with_capacity(0);
107
108        for e in all_cand {
109            let Some(g_spn) = e.generate_spn(&domain_name) else {
110                admin_error!(
111                    uuid = ?e.get_uuid(),
112                    "Entry SPN could not be generated (missing name!?)",
113                );
114                debug_assert!(false);
115                r.push(Err(ConsistencyError::InvalidSpn(e.get_id())));
116                continue;
117            };
118            match e.get_ava_single(Attribute::Spn) {
119                Some(r_spn) => {
120                    trace!("verify spn: s {:?} == ex {:?} ?", r_spn, g_spn);
121                    if r_spn != g_spn {
122                        admin_error!(
123                            uuid = ?e.get_uuid(),
124                            "Entry SPN does not match expected s {:?} != ex {:?}",
125                            r_spn,
126                            g_spn,
127                        );
128                        debug_assert!(false);
129                        r.push(Err(ConsistencyError::InvalidSpn(e.get_id())))
130                    }
131                }
132                None => {
133                    admin_error!(uuid = ?e.get_uuid(), "Entry does not contain an SPN");
134                    r.push(Err(ConsistencyError::InvalidSpn(e.get_id())))
135                }
136            }
137        }
138        r
139    }
140}
141
142impl Spn {
143    fn modify_inner<T: Clone + std::fmt::Debug>(
144        qs: &mut QueryServerWriteTransaction,
145        cand: &mut [Entry<EntryInvalid, T>],
146    ) -> Result<(), OperationError> {
147        let domain_name = qs.get_domain_name();
148
149        for ent in cand.iter_mut() {
150            if ent.attribute_equality(Attribute::Class, &EntryClass::Group.into())
151                || ent.attribute_equality(Attribute::Class, &EntryClass::Account.into())
152            {
153                let spn = ent
154                    .generate_spn(domain_name)
155                    .ok_or(OperationError::InvalidEntryState)
156                    .map_err(|e| {
157                        admin_error!(
158                            "Account or group missing name, unable to generate spn!? {:?} entry_id = {:?}",
159                            e, ent.get_uuid()
160                        );
161                        e
162                    })?;
163                trace!(
164                    "plugin_{}: set {} to {:?}",
165                    Attribute::Spn,
166                    Attribute::Spn,
167                    spn
168                );
169                ent.set_ava(&Attribute::Spn, once(spn));
170            }
171        }
172        Ok(())
173    }
174
175    fn post_modify_inner(
176        qs: &mut QueryServerWriteTransaction,
177        pre_cand: &[Arc<Entry<EntrySealed, EntryCommitted>>],
178        cand: &[Entry<EntrySealed, EntryCommitted>],
179    ) -> Result<(), OperationError> {
180        // On modify, if changing domain_name on UUID_DOMAIN_INFO trigger the spn regen
181
182        let domain_name_changed = cand.iter().zip(pre_cand.iter()).find_map(|(post, pre)| {
183            let domain_name = post.get_ava_single(Attribute::DomainName);
184            if post.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO)
185                && domain_name != pre.get_ava_single(Attribute::DomainName)
186            {
187                domain_name
188            } else {
189                None
190            }
191        });
192
193        let Some(domain_name) = domain_name_changed else {
194            return Ok(());
195        };
196
197        // IMPORTANT - we have to *pre-emptively reload the domain info here*
198        //
199        // If we don't, we don't get the updated domain name in the txn, and then
200        // spn rename fails as we recurse and just populate the old name.
201        qs.reload_domain_info()?;
202
203        admin_info!(
204            "IMPORTANT!!! Changing domain name to \"{:?}\". THIS MAY TAKE A LONG TIME ...",
205            domain_name
206        );
207
208        // All we do is purge spn, and allow the plugin to recreate. Neat! It's also all still
209        // within the transaction, just in case!
210        qs.internal_modify(
211            &filter!(f_pres(Attribute::Spn)),
212            &modlist!([m_purge(Attribute::Spn)]),
213        )
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use crate::prelude::*;
220
221    #[test]
222    fn test_spn_generate_create() {
223        // on create don't provide the spn, we generate it.
224        let e: Entry<EntryInit, EntryNew> = entry_init!(
225            (Attribute::Class, EntryClass::Account.to_value()),
226            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
227            (Attribute::Name, Value::new_iname("testperson")),
228            (Attribute::Description, Value::new_utf8s("testperson")),
229            (Attribute::DisplayName, Value::new_utf8s("testperson"))
230        );
231
232        let create = vec![e];
233        let preload = Vec::with_capacity(0);
234
235        run_create_test!(
236            Ok(()),
237            preload,
238            create,
239            None,
240            |_qs_write: &QueryServerWriteTransaction| {}
241        );
242        // We don't need a validator due to the fn verify above.
243    }
244
245    #[test]
246    fn test_spn_generate_modify() {
247        // on a purge of the spn, generate it.
248        let e: Entry<EntryInit, EntryNew> = entry_init!(
249            (Attribute::Class, EntryClass::Account.to_value()),
250            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
251            (Attribute::Name, Value::new_iname("testperson")),
252            (Attribute::Description, Value::new_utf8s("testperson")),
253            (Attribute::DisplayName, Value::new_utf8s("testperson"))
254        );
255
256        let preload = vec![e];
257
258        run_modify_test!(
259            Ok(()),
260            preload,
261            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
262            modlist!([m_purge(Attribute::Spn)]),
263            None,
264            |_| {},
265            |_| {}
266        );
267    }
268
269    #[test]
270    fn test_spn_validate_create() {
271        // on create providing invalid spn, we over-write it.
272
273        let e: Entry<EntryInit, EntryNew> = entry_init!(
274            (Attribute::Class, EntryClass::Account.to_value()),
275            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
276            (
277                Attribute::Spn,
278                Value::new_utf8s("testperson@invalid_domain.com")
279            ),
280            (Attribute::Name, Value::new_iname("testperson")),
281            (Attribute::Description, Value::new_utf8s("testperson")),
282            (Attribute::DisplayName, Value::new_utf8s("testperson"))
283        );
284
285        let create = vec![e];
286        let preload = Vec::with_capacity(0);
287
288        run_create_test!(
289            Ok(()),
290            preload,
291            create,
292            None,
293            |_qs_write: &QueryServerWriteTransaction| {}
294        );
295    }
296
297    #[test]
298    fn test_spn_validate_modify() {
299        // On modify (removed/present) of the spn, just regenerate it.
300
301        let e: Entry<EntryInit, EntryNew> = entry_init!(
302            (Attribute::Class, EntryClass::Account.to_value()),
303            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
304            (Attribute::Name, Value::new_iname("testperson")),
305            (Attribute::Description, Value::new_utf8s("testperson")),
306            (Attribute::DisplayName, Value::new_utf8s("testperson"))
307        );
308
309        let preload = vec![e];
310
311        run_modify_test!(
312            Ok(()),
313            preload,
314            filter!(f_eq(Attribute::Name, PartialValue::new_iname("testperson"))),
315            modlist!([
316                m_purge(Attribute::Spn),
317                m_pres(
318                    Attribute::Spn,
319                    &Value::new_spn_str("invalid", Attribute::Spn.as_ref())
320                )
321            ]),
322            None,
323            |_| {},
324            |_| {}
325        );
326    }
327
328    #[qs_test]
329    async fn test_spn_regen_domain_rename(server: &QueryServer) {
330        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
331
332        let ex1 = Value::new_spn_str("a_testperson1", "example.com");
333        let ex2 = Value::new_spn_str("a_testperson1", "new.example.com");
334
335        let t_uuid = Uuid::new_v4();
336        let g_uuid = Uuid::new_v4();
337
338        assert!(server_txn
339            .internal_create(vec![
340                entry_init!(
341                    (Attribute::Class, EntryClass::Object.to_value()),
342                    (Attribute::Class, EntryClass::Account.to_value()),
343                    (Attribute::Class, EntryClass::Person.to_value()),
344                    (Attribute::Name, Value::new_iname("a_testperson1")),
345                    (Attribute::Uuid, Value::Uuid(t_uuid)),
346                    (Attribute::Description, Value::new_utf8s("testperson1")),
347                    (Attribute::DisplayName, Value::new_utf8s("testperson1"))
348                ),
349                entry_init!(
350                    (Attribute::Class, EntryClass::Object.to_value()),
351                    (Attribute::Class, EntryClass::Group.to_value()),
352                    (Attribute::Name, Value::new_iname("testgroup")),
353                    (Attribute::Uuid, Value::Uuid(g_uuid)),
354                    (Attribute::Member, Value::Refer(t_uuid))
355                ),
356            ])
357            .is_ok());
358
359        assert!(server_txn.commit().is_ok());
360        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
361
362        // get the current domain name
363        // check the spn on admin is admin@<initial domain>
364        let e_pre = server_txn
365            .internal_search_uuid(t_uuid)
366            .expect("must not fail");
367
368        let e_pre_spn = e_pre.get_ava_single(Attribute::Spn).expect("must not fail");
369        assert_eq!(e_pre_spn, ex1);
370
371        // trigger the domain_name change (this will be a cli option to the server
372        // in the final version), but it will still call the same qs function to perform the
373        // change.
374        server_txn
375            .danger_domain_rename("new.example.com")
376            .expect("should not fail!");
377
378        assert!(server_txn.commit().is_ok());
379        let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
380
381        // check the spn on admin is admin@<new domain>
382        let e_post = server_txn
383            .internal_search_uuid(t_uuid)
384            .expect("must not fail");
385
386        let e_post_spn = e_post
387            .get_ava_single(Attribute::Spn)
388            .expect("must not fail");
389        debug!("{:?}", e_post_spn);
390        debug!("{:?}", ex2);
391        assert_eq!(e_post_spn, ex2);
392
393        // Assert that the uuid2spn index updated.
394        let testuser_spn = server_txn
395            .uuid_to_spn(t_uuid)
396            .expect("Must be able to retrieve the spn")
397            .expect("Value must not be none");
398        assert_eq!(testuser_spn, ex2);
399
400        assert!(server_txn.commit().is_ok());
401    }
402}