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_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 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 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 if let Some(vs) = e.pop_ava(Attribute::TotpImport) {
106 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 if let Some(vs) = e.pop_ava(Attribute::UnixPasswordImport) {
131 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 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 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 #[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}