1use 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 if let Some(vs) = e.pop_ava(Attribute::PasswordImport) {
57 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 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 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 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 if let Some(vs) = e.pop_ava(Attribute::TotpImport) {
104 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 if let Some(vs) = e.pop_ava(Attribute::UnixPasswordImport) {
129 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 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 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 #[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}