1use crate::common::try_expire_at_from_string;
2use crate::OpType;
3use crate::{
4 handle_client_error, password_prompt, AccountCertificate, AccountCredential, AccountRadius,
5 AccountSsh, AccountUserAuthToken, AccountValidity, KanidmClientParser, OutputMode, PersonOpt,
6 PersonPosix,
7};
8use dialoguer::theme::ColorfulTheme;
9use dialoguer::{Confirm, Input, Password, Select};
10use kanidm_client::ClientError::Http as ClientErrorHttp;
11use kanidm_client::KanidmClient;
12use kanidm_proto::attribute::Attribute;
13use kanidm_proto::constants::{
14 ATTR_ACCOUNT_EXPIRE, ATTR_ACCOUNT_SOFTLOCK_EXPIRE, ATTR_ACCOUNT_VALID_FROM, ATTR_GIDNUMBER,
15};
16use kanidm_proto::internal::OperationError::{
17 DuplicateKey, DuplicateLabel, InvalidLabel, NoMatchingEntries, PasswordQuality,
18};
19use kanidm_proto::internal::{
20 CUCredState, CUExtPortal, CUIntentToken, CURegState, CURegWarning, CUSessionToken, CUStatus,
21 SshPublicKey, TotpSecret,
22};
23use kanidm_proto::internal::{CredentialDetail, CredentialDetailType};
24use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
25use kanidm_proto::scim_v1::{client::ScimSshPublicKeys, ScimEntryGetQuery};
26use qrcode::render::unicode;
27use qrcode::QrCode;
28use std::fmt::{self, Debug};
29use std::str::FromStr;
30use time::format_description::well_known::Rfc3339;
31use time::{OffsetDateTime, UtcOffset};
32use uuid::Uuid;
33
34#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
35use crate::webauthn::get_authenticator;
36
37impl PersonOpt {
38 pub async fn exec(&self, opt: KanidmClientParser) {
39 match self {
40 PersonOpt::Credential { commands } => commands.exec(opt).await,
42 PersonOpt::Radius { commands } => match commands {
43 AccountRadius::Show(aopt) => {
44 let client = opt.to_client(OpType::Read).await;
45
46 let rcred = client
47 .idm_account_radius_credential_get(aopt.aopts.account_id.as_str())
48 .await;
49
50 match rcred {
51 Ok(Some(s)) => opt.output_mode.print_message(format!(
52 "RADIUS secret for {}: {}",
53 aopt.aopts.account_id.as_str(),
54 s,
55 )),
56 Ok(None) => opt.output_mode.print_message(format!(
57 "No RADIUS secret set for user {}",
58 aopt.aopts.account_id.as_str(),
59 )),
60 Err(e) => handle_client_error(e, opt.output_mode),
61 }
62 }
63 AccountRadius::Generate(aopt) => {
64 let client = opt.to_client(OpType::Write).await;
65 if let Err(e) = client
66 .idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str())
67 .await
68 {
69 error!("Error -> {:?}", e);
70 }
71 }
72 AccountRadius::DeleteSecret(aopt) => {
73 let client = opt.to_client(OpType::Write).await;
74 let mut modmessage = AccountChangeMessage {
75 output_mode: ConsoleOutputMode::Text,
76 action: "radius account_delete".to_string(),
77 result: "deleted".to_string(),
78 src_user: opt
79 .username
80 .to_owned()
81 .unwrap_or(format!("{:?}", client.whoami().await)),
82 dest_user: aopt.aopts.account_id.to_string(),
83 status: MessageStatus::Success,
84 };
85 match client
86 .idm_account_radius_credential_delete(aopt.aopts.account_id.as_str())
87 .await
88 {
89 Err(e) => {
90 modmessage.status = MessageStatus::Failure;
91 modmessage.result = format!("Error -> {e:?}");
92 error!("{}", modmessage);
93 }
94 Ok(result) => {
95 debug!("{:?}", result);
96 println!("{modmessage}");
97 }
98 };
99 }
100 }, PersonOpt::Posix { commands } => match commands {
102 PersonPosix::Show(aopt) => {
103 let client = opt.to_client(OpType::Read).await;
104 match client
105 .idm_account_unix_token_get(aopt.aopts.account_id.as_str())
106 .await
107 {
108 Ok(token) => println!("{token}"),
109 Err(e) => handle_client_error(e, opt.output_mode),
110 }
111 }
112 PersonPosix::Set(aopt) => {
113 let client = opt.to_client(OpType::Write).await;
114 if let Err(e) = client
115 .idm_person_account_unix_extend(
116 aopt.aopts.account_id.as_str(),
117 aopt.gidnumber,
118 aopt.shell.as_deref(),
119 )
120 .await
121 {
122 handle_client_error(e, opt.output_mode)
123 }
124 }
125 PersonPosix::SetPassword(aopt) => {
126 let client = opt.to_client(OpType::Write).await;
127 let password = match password_prompt("Enter new posix (sudo) password") {
128 Some(v) => v,
129 None => {
130 println!("Passwords do not match");
131 return;
132 }
133 };
134
135 if let Err(e) = client
136 .idm_person_account_unix_cred_put(
137 aopt.aopts.account_id.as_str(),
138 password.as_str(),
139 )
140 .await
141 {
142 handle_client_error(e, opt.output_mode)
143 }
144 }
145 PersonPosix::ResetGidnumber { account_id } => {
146 let client = opt.to_client(OpType::Write).await;
147 if let Err(e) = client
148 .idm_person_account_purge_attr(account_id.as_str(), ATTR_GIDNUMBER)
149 .await
150 {
151 handle_client_error(e, opt.output_mode)
152 }
153 }
154 }, PersonOpt::Session { commands } => match commands {
156 AccountUserAuthToken::Status(apo) => {
157 let client = opt.to_client(OpType::Read).await;
158 match client
159 .idm_account_list_user_auth_token(apo.aopts.account_id.as_str())
160 .await
161 {
162 Ok(tokens) => {
163 if tokens.is_empty() {
164 println!("No sessions exist");
165 } else {
166 for token in tokens {
167 println!("token: {token}");
168 }
169 }
170 }
171 Err(e) => handle_client_error(e, opt.output_mode),
172 }
173 }
174 AccountUserAuthToken::Destroy { aopts, session_id } => {
175 let client = opt.to_client(OpType::Write).await;
176 match client
177 .idm_account_destroy_user_auth_token(aopts.account_id.as_str(), *session_id)
178 .await
179 {
180 Ok(()) => {
181 println!("Success");
182 }
183 Err(e) => {
184 error!("Error destroying account session");
185 handle_client_error(e, opt.output_mode);
186 }
187 }
188 }
189 }, PersonOpt::Ssh { commands } => match commands {
191 AccountSsh::List(aopt) => {
192 let client = opt.to_client(OpType::Read).await;
193
194 let mut entry = match client
195 .scim_v1_person_get(
196 aopt.aopts.account_id.as_str(),
197 Some(ScimEntryGetQuery {
198 attributes: Some(vec![Attribute::SshPublicKey]),
199 ..Default::default()
200 }),
201 )
202 .await
203 {
204 Ok(entry) => entry,
205 Err(e) => return handle_client_error(e, opt.output_mode),
206 };
207
208 let Some(pkeys) = entry.attrs.remove(&Attribute::SshPublicKey) else {
209 println!("No ssh public keys");
210 return;
211 };
212
213 let Ok(keys) = serde_json::from_value::<ScimSshPublicKeys>(pkeys) else {
214 eprintln!("Invalid ssh public key format");
215 return;
216 };
217
218 for key in keys {
219 println!("{}: {}", key.label, key.value);
220 }
221 }
222 AccountSsh::Add(aopt) => {
223 let client = opt.to_client(OpType::Write).await;
224 if let Err(e) = client
225 .idm_person_account_post_ssh_pubkey(
226 aopt.aopts.account_id.as_str(),
227 aopt.tag.as_str(),
228 aopt.pubkey.as_str(),
229 )
230 .await
231 {
232 handle_client_error(e, opt.output_mode)
233 }
234 }
235 AccountSsh::Delete(aopt) => {
236 let client = opt.to_client(OpType::Write).await;
237 if let Err(e) = client
238 .idm_person_account_delete_ssh_pubkey(
239 aopt.aopts.account_id.as_str(),
240 aopt.tag.as_str(),
241 )
242 .await
243 {
244 handle_client_error(e, opt.output_mode)
245 }
246 }
247 }, PersonOpt::List => {
249 let client = opt.to_client(OpType::Read).await;
250 match client.idm_person_account_list().await {
251 Ok(r) => match opt.output_mode {
252 OutputMode::Json => {
253 let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect();
254 println!(
255 "{}",
256 serde_json::to_string(&r_attrs).expect("Failed to serialise json")
257 );
258 }
259 OutputMode::Text => r.iter().for_each(|ent| println!("{ent}")),
260 },
261 Err(e) => handle_client_error(e, opt.output_mode),
262 }
263 }
264 PersonOpt::Search { account_id } => {
265 let client = opt.to_client(OpType::Read).await;
266 match client.idm_person_search(account_id).await {
267 Ok(r) => match opt.output_mode {
268 OutputMode::Json => {
269 let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect();
270 println!(
271 "{}",
272 serde_json::to_string(&r_attrs).expect("Failed to serialise json")
273 );
274 }
275 OutputMode::Text => r.iter().for_each(|ent| println!("{ent}")),
276 },
277 Err(e) => handle_client_error(e, opt.output_mode),
278 }
279 }
280 PersonOpt::Update(aopt) => {
281 let client = opt.to_client(OpType::Write).await;
282 match client
283 .idm_person_account_update(
284 aopt.aopts.account_id.as_str(),
285 aopt.newname.as_deref(),
286 aopt.displayname.as_deref(),
287 aopt.legalname.as_deref(),
288 aopt.mail.as_deref(),
289 )
290 .await
291 {
292 Ok(()) => println!("Success"),
293 Err(e) => handle_client_error(e, opt.output_mode),
294 }
295 }
296 PersonOpt::Get(aopt) => {
297 let client = opt.to_client(OpType::Read).await;
298 match client
299 .idm_person_account_get(aopt.aopts.account_id.as_str())
300 .await
301 {
302 Ok(Some(e)) => match opt.output_mode {
303 OutputMode::Json => {
304 println!(
305 "{}",
306 serde_json::to_string(&e).expect("Failed to serialise json")
307 );
308 }
309 OutputMode::Text => println!("{e}"),
310 },
311 Ok(None) => println!("No matching entries"),
312 Err(e) => handle_client_error(e, opt.output_mode),
313 }
314 }
315 PersonOpt::Delete(aopt) => {
316 let client = opt.to_client(OpType::Write).await;
317 let mut modmessage = AccountChangeMessage {
318 output_mode: ConsoleOutputMode::Text,
319 action: "account delete".to_string(),
320 result: "deleted".to_string(),
321 src_user: opt
322 .username
323 .to_owned()
324 .unwrap_or(format!("{:?}", client.whoami().await)),
325 dest_user: aopt.aopts.account_id.to_string(),
326 status: MessageStatus::Success,
327 };
328 match client
329 .idm_person_account_delete(aopt.aopts.account_id.as_str())
330 .await
331 {
332 Err(e) => {
333 modmessage.result = format!("Error -> {e:?}");
334 modmessage.status = MessageStatus::Failure;
335 eprintln!("{modmessage}");
336
337 }
339 Ok(result) => {
340 debug!("{:?}", result);
341 println!("{modmessage}");
342 }
343 };
344 }
345 PersonOpt::Create(acopt) => {
346 let client = opt.to_client(OpType::Write).await;
347 match client
348 .idm_person_account_create(
349 acopt.aopts.account_id.as_str(),
350 acopt.display_name.as_str(),
351 )
352 .await
353 {
354 Ok(_) => {
355 println!(
356 "Successfully created display_name=\"{}\" username={}",
357 acopt.display_name.as_str(),
358 acopt.aopts.account_id.as_str(),
359 )
360 }
361 Err(e) => handle_client_error(e, opt.output_mode),
362 }
363 }
364 PersonOpt::Validity { commands } => match commands {
365 AccountValidity::Show(ano) => {
366 let client = opt.to_client(OpType::Read).await;
367
368 let entry = match client
369 .idm_person_account_get(ano.aopts.account_id.as_str())
370 .await
371 {
372 Err(err) => {
373 error!(
374 "No account {} found, or other error occurred: {:?}",
375 ano.aopts.account_id.as_str(),
376 err
377 );
378 return;
379 }
380 Ok(val) => match val {
381 Some(val) => val,
382 None => {
383 error!("No account {} found!", ano.aopts.account_id.as_str());
384 return;
385 }
386 },
387 };
388
389 println!("user: {}", ano.aopts.account_id.as_str());
390 if let Some(t) = entry.attrs.get(ATTR_ACCOUNT_VALID_FROM) {
391 let t = OffsetDateTime::parse(&t[0], &Rfc3339)
393 .map(|odt| {
394 odt.to_offset(
395 time::UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH)
396 .unwrap_or(time::UtcOffset::UTC),
397 )
398 .format(&Rfc3339)
399 .unwrap_or(odt.to_string())
400 })
401 .unwrap_or_else(|_| "invalid timestamp".to_string());
402
403 println!("valid after: {t}");
404 } else {
405 println!("valid after: any time");
406 }
407
408 if let Some(t) = entry.attrs.get(ATTR_ACCOUNT_EXPIRE) {
409 let t = OffsetDateTime::parse(&t[0], &Rfc3339)
410 .map(|odt| {
411 odt.to_offset(
412 time::UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH)
413 .unwrap_or(time::UtcOffset::UTC),
414 )
415 .format(&Rfc3339)
416 .unwrap_or(odt.to_string())
417 })
418 .unwrap_or_else(|_| "invalid timestamp".to_string());
419 println!("expire: {t}");
420 } else {
421 println!("expire: never");
422 }
423 }
424 AccountValidity::ExpireAt(ano) => {
425 let client = opt.to_client(OpType::Write).await;
426 let validity = match try_expire_at_from_string(ano.datetime.as_str()) {
427 Ok(val) => val,
428 Err(()) => return,
429 };
430 let res = match validity {
431 None => {
432 client
433 .idm_person_account_purge_attr(
434 ano.aopts.account_id.as_str(),
435 ATTR_ACCOUNT_EXPIRE,
436 )
437 .await
438 }
439 Some(new_expiry) => {
440 client
441 .idm_person_account_set_attr(
442 ano.aopts.account_id.as_str(),
443 ATTR_ACCOUNT_EXPIRE,
444 &[&new_expiry],
445 )
446 .await
447 }
448 };
449 match res {
450 Err(e) => handle_client_error(e, opt.output_mode),
451 _ => println!("Success"),
452 };
453 }
454 AccountValidity::BeginFrom(ano) => {
455 let client = opt.to_client(OpType::Write).await;
456 if matches!(ano.datetime.as_str(), "any" | "clear" | "whenever") {
457 match client
459 .idm_person_account_purge_attr(
460 ano.aopts.account_id.as_str(),
461 ATTR_ACCOUNT_VALID_FROM,
462 )
463 .await
464 {
465 Err(e) => error!(
466 "Error setting begin-from to '{}' -> {:?}",
467 ano.datetime.as_str(),
468 e
469 ),
470 _ => println!("Success"),
471 }
472 } else {
473 if let Err(e) = OffsetDateTime::parse(ano.datetime.as_str(), &Rfc3339) {
475 error!("Error -> {:?}", e);
476 return;
477 }
478
479 match client
480 .idm_person_account_set_attr(
481 ano.aopts.account_id.as_str(),
482 ATTR_ACCOUNT_VALID_FROM,
483 &[ano.datetime.as_str()],
484 )
485 .await
486 {
487 Err(e) => error!(
488 "Error setting begin-from to '{}' -> {:?}",
489 ano.datetime.as_str(),
490 e
491 ),
492 _ => println!("Success"),
493 }
494 }
495 }
496 }, PersonOpt::Certificate { commands } => commands.exec(opt).await,
498 }
499 }
500}
501
502impl AccountCertificate {
503 pub async fn exec(&self, opt: KanidmClientParser) {
504 match self {
505 AccountCertificate::Status { account_id } => {
506 let client = opt.to_client(OpType::Read).await;
507 match client.idm_person_certificate_list(account_id).await {
508 Ok(r) => match opt.output_mode {
509 OutputMode::Json => {
510 let r_attrs: Vec<_> = r.iter().map(|entry| &entry.attrs).collect();
511 println!(
512 "{}",
513 serde_json::to_string(&r_attrs).expect("Failed to serialise json")
514 );
515 }
516 OutputMode::Text => {
517 if r.is_empty() {
518 println!("No certificates available")
519 } else {
520 r.iter().for_each(|ent| println!("{ent}"))
521 }
522 }
523 },
524 Err(e) => handle_client_error(e, opt.output_mode),
525 }
526 }
527 AccountCertificate::Create {
528 account_id,
529 certificate_path,
530 } => {
531 let pem_data = match tokio::fs::read_to_string(certificate_path).await {
532 Ok(pd) => pd,
533 Err(io_err) => {
534 error!(?io_err, ?certificate_path, "Unable to read PEM data");
535 return;
536 }
537 };
538
539 let client = opt.to_client(OpType::Write).await;
540
541 if let Err(e) = client
542 .idm_person_certificate_create(account_id, &pem_data)
543 .await
544 {
545 handle_client_error(e, opt.output_mode);
546 } else {
547 println!("Success");
548 };
549 }
550 }
551 }
552}
553
554impl AccountCredential {
555 pub async fn exec(&self, opt: KanidmClientParser) {
556 match self {
557 AccountCredential::Status(aopt) => {
558 let client = opt.to_client(OpType::Read).await;
559 match client
560 .idm_person_account_get_credential_status(aopt.aopts.account_id.as_str())
561 .await
562 {
563 Ok(cstatus) => {
564 println!("{cstatus}");
565 }
566 Err(e) => {
567 error!("Error getting credential status -> {:?}", e);
568 }
569 }
570 }
571 AccountCredential::Update(aopt) => {
572 let client = opt.to_client(OpType::Write).await;
573 match client
574 .idm_account_credential_update_begin(aopt.aopts.account_id.as_str())
575 .await
576 {
577 Ok((cusession_token, custatus)) => {
578 credential_update_exec(cusession_token, custatus, client).await
579 }
580 Err(e) => {
581 error!("Error starting credential update -> {:?}", e);
582 }
583 }
584 }
585 AccountCredential::UseResetToken(aopt) => {
587 let client = opt.to_unauth_client();
588 let cuintent_token = aopt.token.clone();
589
590 match client
591 .idm_account_credential_update_exchange(cuintent_token)
592 .await
593 {
594 Ok((cusession_token, custatus)) => {
595 credential_update_exec(cusession_token, custatus, client).await
596 }
597 Err(e) => {
598 match e {
599 ClientErrorHttp(status_code, error, _kopid) => {
600 eprintln!(
601 "Error completing command: HTTP{status_code} - {error:?}"
602 );
603 }
604 _ => error!("Error starting use_reset_token -> {:?}", e),
605 };
606 }
607 }
608 }
609 AccountCredential::CreateResetToken { aopts, ttl } => {
610 let client = opt.to_client(OpType::Write).await;
611
612 match client
614 .idm_person_account_credential_update_intent(aopts.account_id.as_str(), *ttl)
615 .await
616 {
617 Ok(CUIntentToken { token, expiry_time }) => {
618 let mut url = client.make_url("/ui/reset");
619 url.query_pairs_mut().append_pair("token", token.as_str());
620
621 debug!(
622 "Successfully created credential reset token for {}: {}",
623 aopts.account_id, token
624 );
625 println!(
626 "The person can use one of the following to allow the credential reset"
627 );
628 println!("\nScan this QR Code:\n");
629 let code = match QrCode::new(url.as_str()) {
630 Ok(c) => c,
631 Err(e) => {
632 error!("Failed to generate QR code -> {:?}", e);
633 return;
634 }
635 };
636 let image = code
637 .render::<unicode::Dense1x2>()
638 .dark_color(unicode::Dense1x2::Light)
639 .light_color(unicode::Dense1x2::Dark)
640 .build();
641 println!("{image}");
642
643 println!();
644 println!("This link: {}", url.as_str());
645 println!(
646 "Or run this command: kanidm person credential use-reset-token {token}"
647 );
648
649 let local_offset =
651 UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
652 let expiry_time = expiry_time.to_offset(local_offset);
653
654 println!(
655 "This token will expire at: {}",
656 expiry_time
657 .format(&Rfc3339)
658 .expect("Failed to format date time!!!")
659 );
660 println!();
661 }
662 Err(e) => {
663 error!("Error starting credential reset -> {:?}", e);
664 }
665 }
666 }
667
668 AccountCredential::SendResetToken {
669 account_id,
670 ttl,
671 alternate_email,
672 } => {
673 let client = opt.to_client(OpType::Write).await;
674
675 if let Err(e) = client
676 .idm_person_account_credential_update_send_intent(
677 account_id,
678 *ttl,
679 alternate_email.clone(),
680 )
681 .await
682 {
683 handle_client_error(e, opt.output_mode);
684 } else {
685 println!("Success");
686 };
687 }
688
689 AccountCredential::SoftlockReset {
690 account_id,
691 datetime,
692 } => {
693 let client = opt.to_client(OpType::Write).await;
694
695 let validity = match try_expire_at_from_string(datetime.as_str()) {
696 Ok(val) => val,
697 Err(()) => return,
698 };
699 let res = match validity {
700 None => {
701 client
702 .idm_person_account_purge_attr(
703 account_id.as_str(),
704 ATTR_ACCOUNT_SOFTLOCK_EXPIRE,
705 )
706 .await
707 }
708 Some(new_expiry) => {
709 client
710 .idm_person_account_set_attr(
711 account_id.as_str(),
712 ATTR_ACCOUNT_SOFTLOCK_EXPIRE,
713 &[&new_expiry],
714 )
715 .await
716 }
717 };
718 match res {
719 Err(e) => handle_client_error(e, opt.output_mode),
720 _ => println!("Success"),
721 };
722 }
723 }
724 }
725}
726
727#[derive(Debug)]
728enum CUAction {
729 Help,
730 Status,
731 Password,
732 Totp,
733 TotpRemove,
734 BackupCodes,
735 Remove,
736 Passkey,
737 PasskeyRemove,
738 AttestedPasskey,
739 AttestedPasskeyRemove,
740 UnixPassword,
741 UnixPasswordRemove,
742 SshPublicKey,
743 SshPublicKeyRemove,
744 End,
745 Commit,
746}
747
748impl fmt::Display for CUAction {
749 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750 write!(
751 f,
752 r#"
753help (h, ?) - Display this help
754status (ls, st) - Show the status of the credential
755end (quit, exit, x, q) - End, without saving any changes
756commit (save) - Commit the changes to the credential
757-- Password and MFA
758password (passwd, pass, pw) - Set a new password
759totp - Generate a new totp, requires a password to be set
760totp remove (totp rm, trm) - Remove the TOTP of this account
761backup codes (bcg, bcode) - (Re)generate backup codes for this account
762remove (rm) - Remove only the password based credential
763-- Passkeys
764passkey (pk) - Add a new Passkey
765passkey remove (passkey rm, pkrm) - Remove a Passkey
766-- Attested Passkeys
767attested-passkey (apk) - Add a new Attested Passkey
768attested-passkey remove (attested-passkey rm, apkrm) - Remove an Attested Passkey
769-- Unix (sudo) Password
770unix-password (upasswd, upass, upw) - Set a new unix/sudo password
771unix-password remove (upassrm upwrm) - Remove the accounts unix password
772-- SSH Public Keys
773ssh-pub-key (ssh, spk) - Add a new ssh public key
774ssh-pub-key remove (sshrm, spkrm) - Remove an ssh public key
775"#
776 )
777 }
778}
779
780impl FromStr for CUAction {
781 type Err = ();
782
783 fn from_str(s: &str) -> Result<Self, Self::Err> {
784 let s = s.to_lowercase();
785 match s.as_str() {
786 "help" | "h" | "?" => Ok(CUAction::Help),
787 "status" | "ls" | "st" => Ok(CUAction::Status),
788 "end" | "quit" | "exit" | "x" | "q" => Ok(CUAction::End),
789 "commit" | "save" => Ok(CUAction::Commit),
790 "password" | "passwd" | "pass" | "pw" => Ok(CUAction::Password),
791 "totp" => Ok(CUAction::Totp),
792 "totp remove" | "totp rm" | "trm" => Ok(CUAction::TotpRemove),
793 "backup codes" | "bcode" | "bcg" => Ok(CUAction::BackupCodes),
794 "remove" | "rm" => Ok(CUAction::Remove),
795 "passkey" | "pk" => Ok(CUAction::Passkey),
796 "passkey remove" | "passkey rm" | "pkrm" => Ok(CUAction::PasskeyRemove),
797 "attested-passkey" | "apk" => Ok(CUAction::AttestedPasskey),
798 "attested-passkey remove" | "attested-passkey rm" | "apkrm" => {
799 Ok(CUAction::AttestedPasskeyRemove)
800 }
801 "unix-password" | "upasswd" | "upass" | "upw" => Ok(CUAction::UnixPassword),
802 "unix-password remove" | "upassrm" | "upwrm" => Ok(CUAction::UnixPasswordRemove),
803
804 "ssh-pub-key" | "ssh" | "spk" => Ok(CUAction::SshPublicKey),
805 "ssh-pub-key remove" | "sshrm" | "spkrm" => Ok(CUAction::SshPublicKeyRemove),
806
807 _ => Err(()),
808 }
809 }
810}
811
812async fn totp_enrol_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
813 let totp_secret: TotpSecret = match client
815 .idm_account_credential_update_init_totp(session_token)
816 .await
817 {
818 Ok(CUStatus {
819 mfaregstate: CURegState::TotpCheck(totp_secret),
820 ..
821 }) => totp_secret,
822 Ok(status) => {
823 debug!(?status);
824 eprintln!("An error occurred -> InvalidState");
825 return;
826 }
827 Err(e) => {
828 eprintln!("An error occurred -> {e:?}");
829 return;
830 }
831 };
832
833 let label: String = Input::new()
834 .with_prompt("TOTP Label")
835 .validate_with(|input: &String| -> Result<(), &str> {
836 if input.trim().is_empty() {
837 Err("Label cannot be empty")
838 } else {
839 Ok(())
840 }
841 })
842 .interact_text()
843 .expect("Failed to interact with interactive session");
844
845 println!("Scan the following QR code with your OTP app.");
847
848 let code = match QrCode::new(totp_secret.to_uri().as_str()) {
849 Ok(c) => c,
850 Err(e) => {
851 error!("Failed to generate QR code -> {:?}", e);
852 return;
853 }
854 };
855 let image = code
856 .render::<unicode::Dense1x2>()
857 .dark_color(unicode::Dense1x2::Light)
858 .light_color(unicode::Dense1x2::Dark)
859 .build();
860 println!("{image}");
861
862 println!("Alternatively, you can manually enter the following OTP details:");
863 println!("--------------------------------------------------------------");
864 println!("TOTP URI: {}", totp_secret.to_uri().as_str());
865 println!("Account Name: {}", totp_secret.accountname);
866 println!("Issuer: {}", totp_secret.issuer);
867 println!("Algorithm: {}", totp_secret.algo);
868 println!("Period/Step: {}", totp_secret.step);
869 println!("Secret: {}", totp_secret.get_secret());
870
871 println!("--------------------------------------------------------------");
873 println!("Enter a TOTP from your authenticator to complete registration:");
874
875 let mut attempts = 3;
877 while attempts > 0 {
878 attempts -= 1;
879 let input: String = Input::new()
881 .with_prompt("TOTP")
882 .validate_with(|input: &String| -> Result<(), &str> {
883 if input.to_lowercase().starts_with('c') || input.trim().parse::<u32>().is_ok() {
884 Ok(())
885 } else {
886 Err("Must be a number (123456) or cancel to end")
887 }
888 })
889 .interact_text()
890 .expect("Failed to interact with interactive session");
891
892 let totp_chal = match input.trim().parse::<u32>() {
894 Ok(v) => v,
895 Err(_) => {
896 eprintln!("Cancelling TOTP registration ...");
897 if let Err(e) = client
898 .idm_account_credential_update_cancel_mfareg(session_token)
899 .await
900 {
901 eprintln!("An error occurred -> {e:?}");
902 } else {
903 println!("success");
904 }
905 return;
906 }
907 };
908 trace!(%totp_chal);
909
910 match client
912 .idm_account_credential_update_check_totp(session_token, totp_chal, &label)
913 .await
914 {
915 Ok(CUStatus {
916 mfaregstate: CURegState::None,
917 ..
918 }) => {
919 println!("success");
920 break;
921 }
922 Ok(CUStatus {
923 mfaregstate: CURegState::TotpTryAgain,
924 ..
925 }) => {
926 eprintln!("Incorrect TOTP code entered. Please try again.");
928 continue;
929 }
930 Ok(CUStatus {
931 mfaregstate: CURegState::TotpNameTryAgain(label),
932 ..
933 }) => {
934 eprintln!("{label} is either invalid or already taken. Please try again.");
935 continue;
936 }
937 Ok(CUStatus {
938 mfaregstate: CURegState::TotpInvalidSha1,
939 ..
940 }) => {
941 eprintln!("⚠️ WARNING - It appears your authenticator app may be broken ⚠️ ");
943 eprintln!(" The TOTP authenticator you are using is forcing the use of SHA1\n");
944 eprintln!(
945 " SHA1 is a deprecated and potentially insecure cryptographic algorithm\n"
946 );
947
948 let items = vec!["Cancel", "I am sure"];
949 let selection = Select::with_theme(&ColorfulTheme::default())
950 .items(&items)
951 .default(0)
952 .interact()
953 .expect("Failed to interact with interactive session");
954
955 match selection {
956 1 => {
957 if let Err(e) = client
958 .idm_account_credential_update_accept_sha1_totp(session_token)
959 .await
960 {
961 eprintln!("An error occurred -> {e:?}");
962 } else {
963 println!("success");
964 }
965 }
966 _ => {
967 println!("Cancelling TOTP registration ...");
968 if let Err(e) = client
969 .idm_account_credential_update_cancel_mfareg(session_token)
970 .await
971 {
972 eprintln!("An error occurred -> {e:?}");
973 } else {
974 println!("success");
975 }
976 }
977 }
978 return;
979 }
980 Ok(status) => {
981 debug!(?status);
982 eprintln!("An error occurred -> InvalidState");
983 return;
984 }
985 Err(e) => {
986 eprintln!("An error occurred -> {e:?}");
987 return;
988 }
989 }
990 }
991 }
993
994#[derive(Clone, Copy)]
995enum PasskeyClass {
996 Any,
997 Attested,
998}
999
1000impl fmt::Display for PasskeyClass {
1001 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1002 match self {
1003 PasskeyClass::Any => write!(f, "Passkey"),
1004 PasskeyClass::Attested => write!(f, "Attested Passkey"),
1005 }
1006 }
1007}
1008
1009#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
1010async fn passkey_enrol_prompt(
1011 _session_token: &CUSessionToken,
1012 _client: &KanidmClient,
1013 _pk_class: PasskeyClass,
1014) {
1015 eprintln!("Passkey enrolment is not supported on this platform");
1016}
1017
1018#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
1019async fn passkey_enrol_prompt(
1020 session_token: &CUSessionToken,
1021 client: &KanidmClient,
1022 pk_class: PasskeyClass,
1023) {
1024 let pk_reg = match pk_class {
1025 PasskeyClass::Any => {
1026 match client
1027 .idm_account_credential_update_passkey_init(session_token)
1028 .await
1029 {
1030 Ok(CUStatus {
1031 mfaregstate: CURegState::Passkey(pk_reg),
1032 ..
1033 }) => pk_reg,
1034 Ok(status) => {
1035 debug!(?status);
1036 eprintln!("An error occurred -> InvalidState");
1037 return;
1038 }
1039 Err(e) => {
1040 eprintln!("An error occurred -> {e:?}");
1041 return;
1042 }
1043 }
1044 }
1045 PasskeyClass::Attested => {
1046 match client
1047 .idm_account_credential_update_attested_passkey_init(session_token)
1048 .await
1049 {
1050 Ok(CUStatus {
1051 mfaregstate: CURegState::AttestedPasskey(pk_reg),
1052 ..
1053 }) => pk_reg,
1054 Ok(status) => {
1055 debug!(?status);
1056 eprintln!("An error occurred -> InvalidState");
1057 return;
1058 }
1059 Err(e) => {
1060 eprintln!("An error occurred -> {e:?}");
1061 return;
1062 }
1063 }
1064 }
1065 };
1066
1067 let mut wa = get_authenticator();
1070
1071 eprintln!("Your authenticator will now flash for you to interact with.");
1072 eprintln!("You may be asked to enter the PIN for your device.");
1073
1074 let rego = match wa.do_registration(client.get_origin().clone(), pk_reg) {
1075 Ok(rego) => rego,
1076 Err(e) => {
1077 error!("Error Signing -> {:?}", e);
1078 return;
1079 }
1080 };
1081
1082 let label: String = Input::new()
1083 .with_prompt("\nEnter a label for this Passkey # ")
1084 .allow_empty(false)
1085 .interact_text()
1086 .expect("Failed to interact with interactive session");
1087
1088 match pk_class {
1089 PasskeyClass::Any => {
1090 match client
1091 .idm_account_credential_update_passkey_finish(session_token, label, rego)
1092 .await
1093 {
1094 Ok(_) => println!("success"),
1095 Err(e) => {
1096 eprintln!("An error occurred -> {e:?}");
1097 }
1098 }
1099 }
1100 PasskeyClass::Attested => {
1101 match client
1102 .idm_account_credential_update_attested_passkey_finish(session_token, label, rego)
1103 .await
1104 {
1105 Ok(_) => println!("success"),
1106 Err(e) => {
1107 eprintln!("An error occurred -> {e:?}");
1108 }
1109 }
1110 }
1111 }
1112}
1113
1114async fn passkey_remove_prompt(
1115 session_token: &CUSessionToken,
1116 client: &KanidmClient,
1117 pk_class: PasskeyClass,
1118) {
1119 match client
1121 .idm_account_credential_update_status(session_token)
1122 .await
1123 {
1124 Ok(status) => match pk_class {
1125 PasskeyClass::Any => {
1126 if status.passkeys.is_empty() {
1127 println!("No passkeys are configured for this user");
1128 return;
1129 }
1130 println!("Current passkeys:");
1131 for pk in status.passkeys {
1132 println!(" {} ({})", pk.tag, pk.uuid);
1133 }
1134 }
1135 PasskeyClass::Attested => {
1136 if status.attested_passkeys.is_empty() {
1137 println!("No attested passkeys are configured for this user");
1138 return;
1139 }
1140 println!("Current attested passkeys:");
1141 for pk in status.attested_passkeys {
1142 println!(" {} ({})", pk.tag, pk.uuid);
1143 }
1144 }
1145 },
1146 Err(e) => {
1147 eprintln!("An error occurred retrieving existing credentials -> {e:?}");
1148 }
1149 }
1150
1151 let uuid_s: String = Input::new()
1152 .with_prompt("\nEnter the UUID of the Passkey to remove (blank to stop) # ")
1153 .validate_with(|input: &String| -> Result<(), &str> {
1154 if input.is_empty() || Uuid::parse_str(input).is_ok() {
1155 Ok(())
1156 } else {
1157 Err("This is not a valid UUID")
1158 }
1159 })
1160 .allow_empty(true)
1161 .interact_text()
1162 .expect("Failed to interact with interactive session");
1163
1164 if let Ok(uuid) = Uuid::parse_str(&uuid_s) {
1166 let result = match pk_class {
1167 PasskeyClass::Any => {
1168 client
1169 .idm_account_credential_update_passkey_remove(session_token, uuid)
1170 .await
1171 }
1172 PasskeyClass::Attested => {
1173 client
1174 .idm_account_credential_update_attested_passkey_remove(session_token, uuid)
1175 .await
1176 }
1177 };
1178
1179 if let Err(e) = result {
1180 eprintln!("An error occurred -> {e:?}");
1181 } else {
1182 println!("success");
1183 }
1184 } else {
1185 println!("{pk_class}s were NOT changed");
1186 }
1187}
1188
1189async fn sshkey_add_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
1190 let ssh_pub_key_str: String = Input::new()
1192 .with_prompt("\nEnter the SSH Public Key (blank to stop) # ")
1193 .validate_with(|input: &String| -> Result<(), &str> {
1194 if input.is_empty() || SshPublicKey::from_string(input).is_ok() {
1195 Ok(())
1196 } else {
1197 Err("This is not a valid SSH Public Key")
1198 }
1199 })
1200 .allow_empty(true)
1201 .interact_text()
1202 .expect("Failed to interact with interactive session");
1203
1204 if ssh_pub_key_str.is_empty() {
1205 println!("SSH Public Key was not added");
1206 return;
1207 }
1208
1209 let ssh_pub_key = match SshPublicKey::from_string(&ssh_pub_key_str) {
1210 Ok(spk) => spk,
1211 Err(_err) => {
1212 eprintln!("Failed to parse ssh public key that previously parsed correctly.");
1213 return;
1214 }
1215 };
1216
1217 let default_label = ssh_pub_key
1218 .comment
1219 .clone()
1220 .unwrap_or_else(|| ssh_pub_key.fingerprint().hash);
1221
1222 loop {
1223 let label: String = Input::new()
1225 .with_prompt("\nEnter the label of the new SSH Public Key")
1226 .default(default_label.clone())
1227 .interact_text()
1228 .expect("Failed to interact with interactive session");
1229
1230 if let Err(err) = client
1231 .idm_account_credential_update_sshkey_add(session_token, label, ssh_pub_key.clone())
1232 .await
1233 {
1234 match err {
1235 ClientErrorHttp(_, Some(InvalidLabel), _) => {
1236 eprintln!("Invalid SSH Public Key label - must only contain letters, numbers, and the characters '@' or '.'");
1237 continue;
1238 }
1239 ClientErrorHttp(_, Some(DuplicateLabel), _) => {
1240 eprintln!("SSH Public Key label already exists - choose another");
1241 continue;
1242 }
1243 ClientErrorHttp(_, Some(DuplicateKey), _) => {
1244 eprintln!("SSH Public Key already exists in this account");
1245 }
1246 _ => eprintln!("An error occurred -> {err:?}"),
1247 }
1248 break;
1249 } else {
1250 println!("Successfully added SSH Public Key");
1251 break;
1252 }
1253 }
1254}
1255
1256async fn sshkey_remove_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
1257 let label: String = Input::new()
1258 .with_prompt("\nEnter the label of the new SSH Public Key (blank to stop) # ")
1259 .allow_empty(true)
1260 .interact_text()
1261 .expect("Failed to interact with interactive session");
1262
1263 if label.is_empty() {
1264 println!("SSH Public Key was NOT removed");
1265 return;
1266 }
1267
1268 if let Err(err) = client
1269 .idm_account_credential_update_sshkey_remove(session_token, label)
1270 .await
1271 {
1272 match err {
1273 ClientErrorHttp(_, Some(NoMatchingEntries), _) => {
1274 eprintln!("SSH Public Key does not exist. Keys were NOT removed.");
1275 }
1276 _ => eprintln!("An error occurred -> {err:?}"),
1277 }
1278 } else {
1279 println!("Successfully removed SSH Public Key");
1280 }
1281}
1282
1283fn display_warnings(warnings: &[CURegWarning]) {
1284 if !warnings.is_empty() {
1285 println!("Warnings:");
1286 }
1287 for warning in warnings {
1288 print!(" ⚠️ ");
1289 match warning {
1290 CURegWarning::MfaRequired => {
1291 println!("Multi-factor authentication required - add TOTP or replace your password with more secure method.");
1292 }
1293 CURegWarning::PasskeyRequired => {
1294 println!("Passkeys required");
1295 }
1296 CURegWarning::AttestedPasskeyRequired => {
1297 println!("Attested Passkeys required");
1298 }
1299 CURegWarning::AttestedResidentKeyRequired => {
1300 println!("Attested Resident Keys required");
1301 }
1302 CURegWarning::WebauthnAttestationUnsatisfiable => {
1303 println!("Attestation is unsatisfiable. Contact your administrator.");
1304 }
1305 CURegWarning::Unsatisfiable => {
1306 println!("Account policy is unsatisfiable. Contact your administrator.");
1307 }
1308 CURegWarning::WebauthnUserVerificationRequired => {
1309 println!(
1310 "The passkey you attempted to register did not provide user verification, please ensure a PIN or equivalent is set."
1311 );
1312 }
1313 CURegWarning::NoValidCredentials => {
1314 println!("Your account has no valid authentication registered - please create at least one credential to proceed.");
1315 }
1316 }
1317 }
1318}
1319
1320fn display_status(status: CUStatus) {
1321 let CUStatus {
1322 spn,
1323 displayname,
1324 ext_cred_portal,
1325 mfaregstate: _,
1326 can_commit,
1327 warnings,
1328 primary,
1329 primary_state,
1330 passkeys,
1331 passkeys_state,
1332 attested_passkeys,
1333 attested_passkeys_state,
1334 attested_passkeys_allowed_devices,
1335 unixcred,
1336 unixcred_state,
1337 sshkeys,
1338 sshkeys_state,
1339 } = status;
1340
1341 println!("spn: {spn}");
1342 println!("Name: {displayname}");
1343
1344 match ext_cred_portal {
1345 CUExtPortal::None => {}
1346 CUExtPortal::Hidden => {
1347 println!("Externally Managed: Not all features may be available");
1348 println!(" Contact your admin for more details.");
1349 }
1350 CUExtPortal::Some(url) => {
1351 println!("Externally Managed: Not all features may be available");
1352 println!(" Visit {} to update your account details.", url.as_str());
1353 }
1354 };
1355
1356 println!("Primary Credential:");
1357
1358 match primary_state {
1359 CUCredState::Modifiable => {
1360 if let Some(cred_detail) = &primary {
1361 print!("{cred_detail}");
1362 } else {
1363 println!(" not set");
1364 }
1365 }
1366 CUCredState::DeleteOnly => {
1367 if let Some(cred_detail) = &primary {
1368 print!("{cred_detail}");
1369 } else {
1370 println!(" unable to modify - access denied");
1371 }
1372 }
1373 CUCredState::AccessDeny => {
1374 println!(" unable to modify - access denied");
1375 }
1376 CUCredState::PolicyDeny => {
1377 println!(" unable to modify - account policy denied");
1378 }
1379 }
1380
1381 println!("Passkeys:");
1382 match passkeys_state {
1383 CUCredState::Modifiable => {
1384 if passkeys.is_empty() {
1385 println!(" not set");
1386 } else {
1387 for pk in passkeys {
1388 println!(" {} ({})", pk.tag, pk.uuid);
1389 }
1390 }
1391 }
1392 CUCredState::DeleteOnly => {
1393 if passkeys.is_empty() {
1394 println!(" unable to modify - access denied");
1395 } else {
1396 for pk in passkeys {
1397 println!(" {} ({})", pk.tag, pk.uuid);
1398 }
1399 }
1400 }
1401 CUCredState::AccessDeny => {
1402 println!(" unable to modify - access denied");
1403 }
1404 CUCredState::PolicyDeny => {
1405 println!(" unable to modify - account policy denied");
1406 }
1407 }
1408
1409 println!("Attested Passkeys:");
1410 match attested_passkeys_state {
1411 CUCredState::Modifiable => {
1412 if attested_passkeys.is_empty() {
1413 println!(" not set");
1414 } else {
1415 for pk in attested_passkeys {
1416 println!(" {} ({})", pk.tag, pk.uuid);
1417 }
1418 }
1419
1420 println!(" --");
1421 println!(" The following devices models are allowed by account policy");
1422 for dev in attested_passkeys_allowed_devices {
1423 println!(" - {dev}");
1424 }
1425 }
1426 CUCredState::DeleteOnly => {
1427 if attested_passkeys.is_empty() {
1428 println!(" unable to modify - attestation policy not configured");
1429 } else {
1430 for pk in attested_passkeys {
1431 println!(" {} ({})", pk.tag, pk.uuid);
1432 }
1433 }
1434 }
1435 CUCredState::AccessDeny => {
1436 println!(" unable to modify - access denied");
1437 }
1438 CUCredState::PolicyDeny => {
1439 println!(" unable to modify - attestation policy not configured");
1440 }
1441 }
1442
1443 println!("Unix (sudo) Password:");
1444 match unixcred_state {
1445 CUCredState::Modifiable => {
1446 if let Some(cred_detail) = &unixcred {
1447 print!("{cred_detail}");
1448 } else {
1449 println!(" not set");
1450 }
1451 }
1452 CUCredState::DeleteOnly => {
1453 if let Some(cred_detail) = &unixcred {
1454 print!("{cred_detail}");
1455 } else {
1456 println!(" unable to modify - access denied");
1457 }
1458 }
1459 CUCredState::AccessDeny => {
1460 println!(" unable to modify - access denied");
1461 }
1462 CUCredState::PolicyDeny => {
1463 println!(" unable to modify - account does not have posix attributes");
1464 }
1465 }
1466
1467 println!("SSH Public Keys:");
1468 match sshkeys_state {
1469 CUCredState::Modifiable => {
1470 if sshkeys.is_empty() {
1471 println!(" not set");
1472 } else {
1473 for (label, sk) in sshkeys {
1474 println!(" {label}: {sk}");
1475 }
1476 }
1477 }
1478 CUCredState::DeleteOnly => {
1479 if sshkeys.is_empty() {
1480 println!(" unable to modify - access denied");
1481 } else {
1482 for (label, sk) in sshkeys {
1483 println!(" {label}: {sk}");
1484 }
1485 }
1486 }
1487 CUCredState::AccessDeny => {
1488 println!(" unable to modify - access denied");
1489 }
1490 CUCredState::PolicyDeny => {
1491 println!(" unable to modify - account policy denied");
1492 }
1493 }
1494
1495 display_warnings(&warnings);
1499
1500 println!("Can Commit: {can_commit}");
1501}
1502
1503async fn credential_update_exec(
1505 session_token: CUSessionToken,
1506 status: CUStatus,
1507 client: KanidmClient,
1508) {
1509 trace!("started credential update exec");
1510 display_status(status);
1512 loop {
1514 let input: String = Input::new()
1516 .with_prompt("\ncred update (? for help) # ")
1517 .validate_with(|input: &String| -> Result<(), &str> {
1518 if CUAction::from_str(input).is_ok() {
1519 Ok(())
1520 } else {
1521 Err("This is not a valid command. See help for valid options (?)")
1522 }
1523 })
1524 .interact_text()
1525 .expect("Failed to interact with interactive session");
1526
1527 let action = match CUAction::from_str(&input) {
1529 Ok(a) => a,
1530 Err(_) => continue,
1531 };
1532
1533 trace!(?action);
1534
1535 match action {
1536 CUAction::Help => {
1537 print!("{action}");
1538 }
1539 CUAction::Status => {
1540 match client
1541 .idm_account_credential_update_status(&session_token)
1542 .await
1543 {
1544 Ok(status) => display_status(status),
1545 Err(e) => {
1546 eprintln!("An error occurred -> {e:?}");
1547 }
1548 }
1549 }
1550 CUAction::Password => {
1551 let password_a = Password::new()
1552 .with_prompt("New password")
1553 .interact()
1554 .expect("Failed to interact with interactive session");
1555 let password_b = Password::new()
1556 .with_prompt("Confirm password")
1557 .interact()
1558 .expect("Failed to interact with interactive session");
1559
1560 if password_a != password_b {
1561 eprintln!("Passwords do not match");
1562 } else if let Err(e) = client
1563 .idm_account_credential_update_set_password(&session_token, &password_a)
1564 .await
1565 {
1566 match e {
1567 ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
1568 eprintln!("Password was not secure enough, please consider the following suggestions:");
1569 for fb_item in feedback.iter() {
1570 eprintln!(" - {fb_item}")
1571 }
1572 }
1573 _ => eprintln!("An error occurred -> {e:?}"),
1574 }
1575 } else {
1576 println!("Successfully reset password.");
1577 }
1578 }
1579 CUAction::Totp => totp_enrol_prompt(&session_token, &client).await,
1580 CUAction::TotpRemove => {
1581 match client
1582 .idm_account_credential_update_status(&session_token)
1583 .await
1584 {
1585 Ok(status) => match status.primary {
1586 Some(CredentialDetail {
1587 uuid: _,
1588 type_: CredentialDetailType::PasswordMfa(totp_labels, ..),
1589 }) => {
1590 if totp_labels.is_empty() {
1591 println!("No totps are configured for this user");
1592 return;
1593 } else {
1594 println!("Current totps:");
1595 for totp_label in totp_labels {
1596 println!(" {totp_label}");
1597 }
1598 }
1599 }
1600 _ => {
1601 println!("No totps are configured for this user");
1602 return;
1603 }
1604 },
1605 Err(e) => {
1606 eprintln!("An error occurred retrieving existing credentials -> {e:?}");
1607 }
1608 }
1609
1610 let label: String = Input::new()
1611 .with_prompt("\nEnter the label of the Passkey to remove (blank to stop) # ")
1612 .allow_empty(true)
1613 .interact_text()
1614 .expect("Failed to interact with interactive session");
1615
1616 if !label.is_empty() {
1617 if let Err(e) = client
1618 .idm_account_credential_update_remove_totp(&session_token, &label)
1619 .await
1620 {
1621 eprintln!("An error occurred -> {e:?}");
1622 } else {
1623 println!("success");
1624 }
1625 } else {
1626 println!("Totp was NOT removed");
1627 }
1628 }
1629 CUAction::BackupCodes => {
1630 match client
1631 .idm_account_credential_update_backup_codes_generate(&session_token)
1632 .await
1633 {
1634 Ok(CUStatus {
1635 mfaregstate: CURegState::BackupCodes(codes),
1636 ..
1637 }) => {
1638 println!("Please store these Backup codes in a safe place");
1639 println!("They will only be displayed ONCE");
1640 for code in codes {
1641 println!(" {code}")
1642 }
1643 }
1644 Ok(status) => {
1645 debug!(?status);
1646 eprintln!("An error occurred -> InvalidState");
1647 }
1648 Err(e) => {
1649 eprintln!("An error occurred -> {e:?}");
1650 }
1651 }
1652 }
1653 CUAction::Remove => {
1654 if Confirm::new()
1655 .with_prompt("Do you want to remove your primary credential?")
1656 .interact()
1657 .expect("Failed to interact with interactive session")
1658 {
1659 if let Err(e) = client
1660 .idm_account_credential_update_primary_remove(&session_token)
1661 .await
1662 {
1663 eprintln!("An error occurred -> {e:?}");
1664 } else {
1665 println!("success");
1666 }
1667 } else {
1668 println!("Primary credential was NOT removed");
1669 }
1670 }
1671 CUAction::Passkey => {
1672 passkey_enrol_prompt(&session_token, &client, PasskeyClass::Any).await
1673 }
1674 CUAction::PasskeyRemove => {
1675 passkey_remove_prompt(&session_token, &client, PasskeyClass::Any).await
1676 }
1677 CUAction::AttestedPasskey => {
1678 passkey_enrol_prompt(&session_token, &client, PasskeyClass::Attested).await
1679 }
1680 CUAction::AttestedPasskeyRemove => {
1681 passkey_remove_prompt(&session_token, &client, PasskeyClass::Attested).await
1682 }
1683
1684 CUAction::UnixPassword => {
1685 let password_a = Password::new()
1686 .with_prompt("New Unix Password")
1687 .interact()
1688 .expect("Failed to interact with interactive session");
1689 let password_b = Password::new()
1690 .with_prompt("Confirm password")
1691 .interact()
1692 .expect("Failed to interact with interactive session");
1693
1694 if password_a != password_b {
1695 eprintln!("Passwords do not match");
1696 } else if let Err(e) = client
1697 .idm_account_credential_update_set_unix_password(&session_token, &password_a)
1698 .await
1699 {
1700 match e {
1701 ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
1702 eprintln!("Password was not secure enough, please consider the following suggestions:");
1703 for fb_item in feedback.iter() {
1704 eprintln!(" - {fb_item}")
1705 }
1706 }
1707 _ => eprintln!("An error occurred -> {e:?}"),
1708 }
1709 } else {
1710 println!("Successfully reset unix password.");
1711 }
1712 }
1713
1714 CUAction::UnixPasswordRemove => {
1715 if Confirm::new()
1716 .with_prompt("Do you want to remove your unix password?")
1717 .interact()
1718 .expect("Failed to interact with interactive session")
1719 {
1720 if let Err(e) = client
1721 .idm_account_credential_update_unix_remove(&session_token)
1722 .await
1723 {
1724 eprintln!("An error occurred -> {e:?}");
1725 } else {
1726 println!("success");
1727 }
1728 } else {
1729 println!("unix password was NOT removed");
1730 }
1731 }
1732 CUAction::SshPublicKey => sshkey_add_prompt(&session_token, &client).await,
1733 CUAction::SshPublicKeyRemove => sshkey_remove_prompt(&session_token, &client).await,
1734 CUAction::End => {
1735 println!("Changes were NOT saved.");
1736 break;
1737 }
1738 CUAction::Commit => {
1739 match client
1740 .idm_account_credential_update_status(&session_token)
1741 .await
1742 {
1743 Ok(status) => {
1744 if !status.can_commit {
1745 display_warnings(&status.warnings);
1746 println!("Changes have NOT been saved.");
1748 continue;
1749 }
1750 }
1752 Err(e) => {
1753 eprintln!("An error occurred -> {e:?}");
1754 }
1755 }
1756
1757 if Confirm::new()
1758 .with_prompt("Do you want to commit your changes?")
1759 .interact()
1760 .expect("Failed to interact with interactive session")
1761 {
1762 if let Err(e) = client
1763 .idm_account_credential_update_commit(&session_token)
1764 .await
1765 {
1766 eprintln!("An error occurred -> {e:?}");
1767 println!("Changes have NOT been saved.");
1768 } else {
1769 println!("Success - Changes have been saved.");
1770 break;
1771 }
1772 } else {
1773 println!("Changes have NOT been saved.");
1774 }
1775 }
1776 }
1777 }
1778 trace!("ended credential update exec");
1779}