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