kanidmd_lib/plugins/
cred_import.rs

1// Transform password import requests into proper kanidm credentials.
2use std::convert::TryFrom;
3use std::iter::once;
4use std::sync::Arc;
5
6use crate::credential::{Credential, Password};
7use crate::event::{CreateEvent, ModifyEvent};
8use crate::plugins::Plugin;
9use crate::prelude::*;
10
11pub struct CredImport {}
12
13impl Plugin for CredImport {
14    fn id() -> &'static str {
15        "plugin_password_import"
16    }
17
18    #[instrument(
19        level = "debug",
20        name = "password_import_pre_create_transform",
21        skip_all
22    )]
23    fn pre_create_transform(
24        _qs: &mut QueryServerWriteTransaction,
25        cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
26        _ce: &CreateEvent,
27    ) -> Result<(), OperationError> {
28        Self::modify_inner(cand)
29    }
30
31    #[instrument(level = "debug", name = "password_import_pre_modify", skip_all)]
32    fn pre_modify(
33        _qs: &mut QueryServerWriteTransaction,
34        _pre_cand: &[Arc<EntrySealedCommitted>],
35        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
36        _me: &ModifyEvent,
37    ) -> Result<(), OperationError> {
38        Self::modify_inner(cand)
39    }
40
41    #[instrument(level = "debug", name = "password_import_pre_batch_modify", skip_all)]
42    fn pre_batch_modify(
43        _qs: &mut QueryServerWriteTransaction,
44        _pre_cand: &[Arc<EntrySealedCommitted>],
45        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
46        _me: &BatchModifyEvent,
47    ) -> Result<(), OperationError> {
48        Self::modify_inner(cand)
49    }
50}
51
52impl CredImport {
53    fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> Result<(), OperationError> {
54        cand.iter_mut().try_for_each(|e| {
55            // PASSWORD IMPORT
56            if let Some(vs) = e.pop_ava(Attribute::PasswordImport) {
57                // if there are multiple, fail.
58                let im_pw = vs.to_utf8_single().ok_or_else(|| {
59                    OperationError::Plugin(PluginError::CredImport(
60                        format!("{} has incorrect value type - should be a single utf8 string", Attribute::PasswordImport),
61                    ))
62                })?;
63
64                // convert the import_password_string to a password
65                let pw = Password::try_from(im_pw).map_err(|_| {
66                    let len = if im_pw.len() > 5 {
67                        4
68                    } else {
69                        im_pw.len() - 1
70                    };
71                    let hint = im_pw.split_at_checked(len)
72                        .map(|(a, _)| a)
73                        .unwrap_or("CORRUPT");
74                    let id = e.get_display_id();
75
76                    error!(%hint, entry_id = %id, "{} was unable to convert hash format", Attribute::PasswordImport);
77
78                    OperationError::Plugin(PluginError::CredImport(
79                        "password_import was unable to convert hash format".to_string(),
80                    ))
81                })?;
82
83                // does the entry have a primary cred?
84                match e.get_ava_single_credential(Attribute::PrimaryCredential) {
85                    Some(c) => {
86                        let c = c.update_password(pw);
87                        e.set_ava(
88                            &Attribute::PrimaryCredential,
89                            once(Value::new_credential("primary", c)),
90                        );
91                    }
92                    None => {
93                        // just set it then!
94                        let c = Credential::new_from_password(pw);
95                        e.set_ava(
96                            &Attribute::PrimaryCredential,
97                            once(Value::new_credential("primary", c)),
98                        );
99                    }
100                }
101            };
102
103            // TOTP IMPORT - Must be subsequent to password import to allow primary cred to
104            // be created.
105            if let Some(vs) = e.pop_ava(Attribute::TotpImport) {
106                // Get the map.
107                let totps = vs.as_totp_map().ok_or_else(|| {
108                    OperationError::Plugin(PluginError::CredImport(
109                        format!("{} has incorrect value type - should be a map of totp", Attribute::TotpImport)
110                    ))
111                })?;
112
113                if let Some(c) = e.get_ava_single_credential(Attribute::PrimaryCredential) {
114                    let c = totps.iter().fold(c.clone(), |acc, (label, totp)| {
115                        acc.append_totp(label.clone(), totp.clone())
116                    });
117                    e.set_ava(
118                        &Attribute::PrimaryCredential,
119                        once(Value::new_credential("primary", c)),
120                    );
121                } else {
122                    return Err(OperationError::Plugin(PluginError::CredImport(
123                        format!("{} can not be used if {} (password) is missing"
124                            ,Attribute::TotpImport, Attribute::PrimaryCredential),
125                    )));
126                }
127            }
128
129            // UNIX PASSWORD IMPORT
130            if let Some(vs) = e.pop_ava(Attribute::UnixPasswordImport) {
131                // if there are multiple, fail.
132                let im_pw = vs.to_utf8_single().ok_or_else(|| {
133                    OperationError::Plugin(PluginError::CredImport(
134                        format!("{} has incorrect value type - should be a single utf8 string", Attribute::UnixPasswordImport),
135                    ))
136                })?;
137
138                // convert the import_password_string to a password
139                let pw = Password::try_from(im_pw).map_err(|_| {
140                    let len = if im_pw.len() > 5 {
141                        4
142                    } else {
143                        im_pw.len() - 1
144                    };
145                    let hint = im_pw.split_at_checked(len)
146                        .map(|(a, _)| a)
147                        .unwrap_or("CORRUPT");
148                    let id = e.get_display_id();
149
150                    error!(%hint, entry_id = %id, "{} was unable to convert hash format", Attribute::UnixPasswordImport);
151
152                    OperationError::Plugin(PluginError::CredImport(
153                        "unix_password_import was unable to convert hash format".to_string(),
154                    ))
155                })?;
156
157                // Unix pw's aren't like primary, we can just splat them here.
158                let c = Credential::new_from_password(pw);
159                e.set_ava(
160                    &Attribute::UnixPassword,
161                    once(Value::new_credential("primary", c)),
162                );
163            };
164
165            Ok(())
166        })
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
173    use crate::credential::{Credential, CredentialType};
174    use crate::prelude::*;
175    use kanidm_lib_crypto::CryptoPolicy;
176
177    const IMPORT_HASH: &str =
178        "pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w=";
179    // const IMPORT_PASSWORD: &'static str = "eicieY7ahchaoCh0eeTa";
180
181    #[test]
182    fn test_pre_create_password_import_1() {
183        let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
184
185        let e = entry_init!(
186            (Attribute::Class, EntryClass::Account.to_value()),
187            (Attribute::Class, EntryClass::Person.to_value()),
188            (Attribute::Name, Value::new_iname("testperson")),
189            (
190                Attribute::Description,
191                Value::Utf8("testperson".to_string())
192            ),
193            (
194                Attribute::DisplayName,
195                Value::Utf8("testperson".to_string())
196            ),
197            (
198                Attribute::Uuid,
199                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
200            ),
201            (
202                Attribute::PasswordImport,
203                Value::Utf8(
204                    "pbkdf2_sha256$36000$xIEozuZVAoYm$uW1b35DUKyhvQAf1mBqMvoBDcqSD06juzyO/nmyV0+w="
205                        .into()
206                )
207            )
208        );
209
210        let create = vec![e];
211
212        run_create_test!(Ok(()), preload, create, None, |_| {});
213    }
214
215    #[test]
216    fn test_modify_password_import_1() {
217        let ea = entry_init!(
218            (Attribute::Class, EntryClass::Account.to_value()),
219            (Attribute::Class, EntryClass::Person.to_value()),
220            (Attribute::Name, Value::new_iname("testperson")),
221            (
222                Attribute::Description,
223                Value::Utf8("testperson".to_string())
224            ),
225            (
226                Attribute::DisplayName,
227                Value::Utf8("testperson".to_string())
228            ),
229            (
230                Attribute::Uuid,
231                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
232            )
233        );
234
235        let preload = vec![ea];
236
237        run_modify_test!(
238            Ok(()),
239            preload,
240            filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
241            ModifyList::new_list(vec![Modify::Present(
242                Attribute::PasswordImport,
243                Value::from(IMPORT_HASH)
244            )]),
245            None,
246            |_| {},
247            |_| {}
248        );
249    }
250
251    #[test]
252    fn test_modify_password_import_2() {
253        let mut ea = entry_init!(
254            (Attribute::Class, EntryClass::Account.to_value()),
255            (Attribute::Class, EntryClass::Person.to_value()),
256            (Attribute::Name, Value::new_iname("testperson")),
257            (
258                Attribute::Description,
259                Value::Utf8("testperson".to_string())
260            ),
261            (
262                Attribute::DisplayName,
263                Value::Utf8("testperson".to_string())
264            ),
265            (
266                Attribute::Uuid,
267                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
268            )
269        );
270
271        let p = CryptoPolicy::minimum();
272        let c = Credential::new_password_only(&p, "password").unwrap();
273        ea.add_ava(
274            Attribute::PrimaryCredential,
275            Value::new_credential("primary", c),
276        );
277
278        let preload = vec![ea];
279
280        run_modify_test!(
281            Ok(()),
282            preload,
283            filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
284            ModifyList::new_list(vec![Modify::Present(
285                Attribute::PasswordImport,
286                Value::from(IMPORT_HASH)
287            )]),
288            None,
289            |_| {},
290            |_| {}
291        );
292    }
293
294    #[test]
295    fn test_modify_password_import_3_totp() {
296        let mut ea = entry_init!(
297            (Attribute::Class, EntryClass::Account.to_value()),
298            (Attribute::Class, EntryClass::Person.to_value()),
299            (Attribute::Name, Value::new_iname("testperson")),
300            (
301                Attribute::Description,
302                Value::Utf8("testperson".to_string())
303            ),
304            (
305                Attribute::DisplayName,
306                Value::Utf8("testperson".to_string())
307            ),
308            (
309                Attribute::Uuid,
310                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
311            )
312        );
313
314        let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
315        let p = CryptoPolicy::minimum();
316        let c = Credential::new_password_only(&p, "password")
317            .unwrap()
318            .append_totp("totp".to_string(), totp);
319        ea.add_ava(
320            Attribute::PrimaryCredential,
321            Value::new_credential("primary", c),
322        );
323
324        let preload = vec![ea];
325
326        run_modify_test!(
327            Ok(()),
328            preload,
329            filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
330            ModifyList::new_list(vec![Modify::Present(
331                Attribute::PasswordImport,
332                Value::from(IMPORT_HASH)
333            )]),
334            None,
335            |_| {},
336            |qs: &mut QueryServerWriteTransaction| {
337                let e = qs
338                    .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
339                    .expect("failed to get entry");
340                let c = e
341                    .get_ava_single_credential(Attribute::PrimaryCredential)
342                    .expect("failed to get primary cred.");
343                match &c.type_ {
344                    CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
345                        assert_eq!(totp.len(), 1);
346                        assert!(webauthn.is_empty());
347                        assert!(backup_code.is_none());
348                    }
349                    _ => panic!("Oh no"),
350                };
351            }
352        );
353    }
354
355    #[test]
356    fn test_modify_cred_import_pw_and_multi_totp() {
357        let euuid = Uuid::new_v4();
358
359        let ea = entry_init!(
360            (Attribute::Class, EntryClass::Account.to_value()),
361            (Attribute::Class, EntryClass::Person.to_value()),
362            (Attribute::Name, Value::new_iname("testperson")),
363            (
364                Attribute::Description,
365                Value::Utf8("testperson".to_string())
366            ),
367            (
368                Attribute::DisplayName,
369                Value::Utf8("testperson".to_string())
370            ),
371            (Attribute::Uuid, Value::Uuid(euuid))
372        );
373
374        let preload = vec![ea];
375
376        let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
377        let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
378
379        run_modify_test!(
380            Ok(()),
381            preload,
382            filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
383            ModifyList::new_list(vec![
384                Modify::Present(
385                    Attribute::PasswordImport,
386                    Value::Utf8(IMPORT_HASH.to_string())
387                ),
388                Modify::Present(
389                    Attribute::TotpImport,
390                    Value::TotpSecret("a".to_string(), totp_a.clone())
391                ),
392                Modify::Present(
393                    Attribute::TotpImport,
394                    Value::TotpSecret("b".to_string(), totp_b.clone())
395                )
396            ]),
397            None,
398            |_| {},
399            |qs: &mut QueryServerWriteTransaction| {
400                let e = qs.internal_search_uuid(euuid).expect("failed to get entry");
401                let c = e
402                    .get_ava_single_credential(Attribute::PrimaryCredential)
403                    .expect("failed to get primary cred.");
404                match &c.type_ {
405                    CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
406                        assert_eq!(totp.len(), 2);
407                        assert!(webauthn.is_empty());
408                        assert!(backup_code.is_none());
409
410                        assert_eq!(totp.get("a"), Some(&totp_a));
411                        assert_eq!(totp.get("b"), Some(&totp_b));
412                    }
413                    _ => panic!("Oh no"),
414                };
415            }
416        );
417    }
418
419    #[test]
420    fn test_modify_cred_import_pw_missing_with_totp() {
421        let euuid = Uuid::new_v4();
422
423        let ea = entry_init!(
424            (Attribute::Class, EntryClass::Account.to_value()),
425            (Attribute::Class, EntryClass::Person.to_value()),
426            (Attribute::Name, Value::new_iname("testperson")),
427            (
428                Attribute::Description,
429                Value::Utf8("testperson".to_string())
430            ),
431            (
432                Attribute::DisplayName,
433                Value::Utf8("testperson".to_string())
434            ),
435            (Attribute::Uuid, Value::Uuid(euuid))
436        );
437
438        let preload = vec![ea];
439
440        let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
441
442        run_modify_test!(
443            Err(OperationError::Plugin(PluginError::CredImport(
444                "totp_import can not be used if primary_credential (password) is missing"
445                    .to_string()
446            ))),
447            preload,
448            filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
449            ModifyList::new_list(vec![Modify::Present(
450                Attribute::TotpImport,
451                Value::TotpSecret("a".to_string(), totp_a)
452            )]),
453            None,
454            |_| {},
455            |_| {}
456        );
457    }
458
459    #[test]
460    fn test_modify_unix_password_import() {
461        let ea = entry_init!(
462            (Attribute::Class, EntryClass::Account.to_value()),
463            (Attribute::Class, EntryClass::PosixAccount.to_value()),
464            (Attribute::Class, EntryClass::Person.to_value()),
465            (Attribute::Name, Value::new_iname("testperson")),
466            (
467                Attribute::Description,
468                Value::Utf8("testperson".to_string())
469            ),
470            (
471                Attribute::DisplayName,
472                Value::Utf8("testperson".to_string())
473            ),
474            (
475                Attribute::Uuid,
476                Value::Uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
477            )
478        );
479
480        let preload = vec![ea];
481
482        run_modify_test!(
483            Ok(()),
484            preload,
485            filter!(f_eq(Attribute::Name, PartialValue::new_iutf8("testperson"))),
486            ModifyList::new_list(vec![Modify::Present(
487                Attribute::UnixPasswordImport,
488                Value::from(IMPORT_HASH)
489            )]),
490            None,
491            |_| {},
492            |qs: &mut QueryServerWriteTransaction| {
493                let e = qs
494                    .internal_search_uuid(uuid!("d2b496bd-8493-47b7-8142-f568b5cf47ee"))
495                    .expect("failed to get entry");
496                let c = e
497                    .get_ava_single_credential(Attribute::UnixPassword)
498                    .expect("failed to get unix cred.");
499
500                assert!(matches!(&c.type_, CredentialType::Password(_pw)));
501            }
502        );
503    }
504}