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(|entry| {
55 if let Some(vs) = entry.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(|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 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 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 if let Some(vs) = entry.pop_ava(Attribute::TotpImport) {
95 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 if let Some(vs) = entry.pop_ava(Attribute::UnixPasswordImport) {
120 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 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 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 #[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}