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