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::SoftlockReset {
669 account_id,
670 datetime,
671 } => {
672 let client = opt.to_client(OpType::Write).await;
673
674 let validity = match try_expire_at_from_string(datetime.as_str()) {
675 Ok(val) => val,
676 Err(()) => return,
677 };
678 let res = match validity {
679 None => {
680 client
681 .idm_person_account_purge_attr(
682 account_id.as_str(),
683 ATTR_ACCOUNT_SOFTLOCK_EXPIRE,
684 )
685 .await
686 }
687 Some(new_expiry) => {
688 client
689 .idm_person_account_set_attr(
690 account_id.as_str(),
691 ATTR_ACCOUNT_SOFTLOCK_EXPIRE,
692 &[&new_expiry],
693 )
694 .await
695 }
696 };
697 match res {
698 Err(e) => handle_client_error(e, opt.output_mode),
699 _ => println!("Success"),
700 };
701 }
702 }
703 }
704}
705
706#[derive(Debug)]
707enum CUAction {
708 Help,
709 Status,
710 Password,
711 Totp,
712 TotpRemove,
713 BackupCodes,
714 Remove,
715 Passkey,
716 PasskeyRemove,
717 AttestedPasskey,
718 AttestedPasskeyRemove,
719 UnixPassword,
720 UnixPasswordRemove,
721 SshPublicKey,
722 SshPublicKeyRemove,
723 End,
724 Commit,
725}
726
727impl fmt::Display for CUAction {
728 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729 write!(
730 f,
731 r#"
732help (h, ?) - Display this help
733status (ls, st) - Show the status of the credential
734end (quit, exit, x, q) - End, without saving any changes
735commit (save) - Commit the changes to the credential
736-- Password and MFA
737password (passwd, pass, pw) - Set a new password
738totp - Generate a new totp, requires a password to be set
739totp remove (totp rm, trm) - Remove the TOTP of this account
740backup codes (bcg, bcode) - (Re)generate backup codes for this account
741remove (rm) - Remove only the password based credential
742-- Passkeys
743passkey (pk) - Add a new Passkey
744passkey remove (passkey rm, pkrm) - Remove a Passkey
745-- Attested Passkeys
746attested-passkey (apk) - Add a new Attested Passkey
747attested-passkey remove (attested-passkey rm, apkrm) - Remove an Attested Passkey
748-- Unix (sudo) Password
749unix-password (upasswd, upass, upw) - Set a new unix/sudo password
750unix-password remove (upassrm upwrm) - Remove the accounts unix password
751-- SSH Public Keys
752ssh-pub-key (ssh, spk) - Add a new ssh public key
753ssh-pub-key remove (sshrm, spkrm) - Remove an ssh public key
754"#
755 )
756 }
757}
758
759impl FromStr for CUAction {
760 type Err = ();
761
762 fn from_str(s: &str) -> Result<Self, Self::Err> {
763 let s = s.to_lowercase();
764 match s.as_str() {
765 "help" | "h" | "?" => Ok(CUAction::Help),
766 "status" | "ls" | "st" => Ok(CUAction::Status),
767 "end" | "quit" | "exit" | "x" | "q" => Ok(CUAction::End),
768 "commit" | "save" => Ok(CUAction::Commit),
769 "password" | "passwd" | "pass" | "pw" => Ok(CUAction::Password),
770 "totp" => Ok(CUAction::Totp),
771 "totp remove" | "totp rm" | "trm" => Ok(CUAction::TotpRemove),
772 "backup codes" | "bcode" | "bcg" => Ok(CUAction::BackupCodes),
773 "remove" | "rm" => Ok(CUAction::Remove),
774 "passkey" | "pk" => Ok(CUAction::Passkey),
775 "passkey remove" | "passkey rm" | "pkrm" => Ok(CUAction::PasskeyRemove),
776 "attested-passkey" | "apk" => Ok(CUAction::AttestedPasskey),
777 "attested-passkey remove" | "attested-passkey rm" | "apkrm" => {
778 Ok(CUAction::AttestedPasskeyRemove)
779 }
780 "unix-password" | "upasswd" | "upass" | "upw" => Ok(CUAction::UnixPassword),
781 "unix-password remove" | "upassrm" | "upwrm" => Ok(CUAction::UnixPasswordRemove),
782
783 "ssh-pub-key" | "ssh" | "spk" => Ok(CUAction::SshPublicKey),
784 "ssh-pub-key remove" | "sshrm" | "spkrm" => Ok(CUAction::SshPublicKeyRemove),
785
786 _ => Err(()),
787 }
788 }
789}
790
791async fn totp_enrol_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
792 let totp_secret: TotpSecret = match client
794 .idm_account_credential_update_init_totp(session_token)
795 .await
796 {
797 Ok(CUStatus {
798 mfaregstate: CURegState::TotpCheck(totp_secret),
799 ..
800 }) => totp_secret,
801 Ok(status) => {
802 debug!(?status);
803 eprintln!("An error occurred -> InvalidState");
804 return;
805 }
806 Err(e) => {
807 eprintln!("An error occurred -> {e:?}");
808 return;
809 }
810 };
811
812 let label: String = Input::new()
813 .with_prompt("TOTP Label")
814 .validate_with(|input: &String| -> Result<(), &str> {
815 if input.trim().is_empty() {
816 Err("Label cannot be empty")
817 } else {
818 Ok(())
819 }
820 })
821 .interact_text()
822 .expect("Failed to interact with interactive session");
823
824 println!("Scan the following QR code with your OTP app.");
826
827 let code = match QrCode::new(totp_secret.to_uri().as_str()) {
828 Ok(c) => c,
829 Err(e) => {
830 error!("Failed to generate QR code -> {:?}", e);
831 return;
832 }
833 };
834 let image = code
835 .render::<unicode::Dense1x2>()
836 .dark_color(unicode::Dense1x2::Light)
837 .light_color(unicode::Dense1x2::Dark)
838 .build();
839 println!("{image}");
840
841 println!("Alternatively, you can manually enter the following OTP details:");
842 println!("--------------------------------------------------------------");
843 println!("TOTP URI: {}", totp_secret.to_uri().as_str());
844 println!("Account Name: {}", totp_secret.accountname);
845 println!("Issuer: {}", totp_secret.issuer);
846 println!("Algorithm: {}", totp_secret.algo);
847 println!("Period/Step: {}", totp_secret.step);
848 println!("Secret: {}", totp_secret.get_secret());
849
850 println!("--------------------------------------------------------------");
852 println!("Enter a TOTP from your authenticator to complete registration:");
853
854 let mut attempts = 3;
856 while attempts > 0 {
857 attempts -= 1;
858 let input: String = Input::new()
860 .with_prompt("TOTP")
861 .validate_with(|input: &String| -> Result<(), &str> {
862 if input.to_lowercase().starts_with('c') || input.trim().parse::<u32>().is_ok() {
863 Ok(())
864 } else {
865 Err("Must be a number (123456) or cancel to end")
866 }
867 })
868 .interact_text()
869 .expect("Failed to interact with interactive session");
870
871 let totp_chal = match input.trim().parse::<u32>() {
873 Ok(v) => v,
874 Err(_) => {
875 eprintln!("Cancelling TOTP registration ...");
876 if let Err(e) = client
877 .idm_account_credential_update_cancel_mfareg(session_token)
878 .await
879 {
880 eprintln!("An error occurred -> {e:?}");
881 } else {
882 println!("success");
883 }
884 return;
885 }
886 };
887 trace!(%totp_chal);
888
889 match client
891 .idm_account_credential_update_check_totp(session_token, totp_chal, &label)
892 .await
893 {
894 Ok(CUStatus {
895 mfaregstate: CURegState::None,
896 ..
897 }) => {
898 println!("success");
899 break;
900 }
901 Ok(CUStatus {
902 mfaregstate: CURegState::TotpTryAgain,
903 ..
904 }) => {
905 eprintln!("Incorrect TOTP code entered. Please try again.");
907 continue;
908 }
909 Ok(CUStatus {
910 mfaregstate: CURegState::TotpNameTryAgain(label),
911 ..
912 }) => {
913 eprintln!("{label} is either invalid or already taken. Please try again.");
914 continue;
915 }
916 Ok(CUStatus {
917 mfaregstate: CURegState::TotpInvalidSha1,
918 ..
919 }) => {
920 eprintln!("⚠️ WARNING - It appears your authenticator app may be broken ⚠️ ");
922 eprintln!(" The TOTP authenticator you are using is forcing the use of SHA1\n");
923 eprintln!(
924 " SHA1 is a deprecated and potentially insecure cryptographic algorithm\n"
925 );
926
927 let items = vec!["Cancel", "I am sure"];
928 let selection = Select::with_theme(&ColorfulTheme::default())
929 .items(&items)
930 .default(0)
931 .interact()
932 .expect("Failed to interact with interactive session");
933
934 match selection {
935 1 => {
936 if let Err(e) = client
937 .idm_account_credential_update_accept_sha1_totp(session_token)
938 .await
939 {
940 eprintln!("An error occurred -> {e:?}");
941 } else {
942 println!("success");
943 }
944 }
945 _ => {
946 println!("Cancelling TOTP registration ...");
947 if let Err(e) = client
948 .idm_account_credential_update_cancel_mfareg(session_token)
949 .await
950 {
951 eprintln!("An error occurred -> {e:?}");
952 } else {
953 println!("success");
954 }
955 }
956 }
957 return;
958 }
959 Ok(status) => {
960 debug!(?status);
961 eprintln!("An error occurred -> InvalidState");
962 return;
963 }
964 Err(e) => {
965 eprintln!("An error occurred -> {e:?}");
966 return;
967 }
968 }
969 }
970 }
972
973#[derive(Clone, Copy)]
974enum PasskeyClass {
975 Any,
976 Attested,
977}
978
979impl fmt::Display for PasskeyClass {
980 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
981 match self {
982 PasskeyClass::Any => write!(f, "Passkey"),
983 PasskeyClass::Attested => write!(f, "Attested Passkey"),
984 }
985 }
986}
987
988#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
989async fn passkey_enrol_prompt(
990 _session_token: &CUSessionToken,
991 _client: &KanidmClient,
992 _pk_class: PasskeyClass,
993) {
994 eprintln!("Passkey enrolment is not supported on this platform");
995}
996
997#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
998async fn passkey_enrol_prompt(
999 session_token: &CUSessionToken,
1000 client: &KanidmClient,
1001 pk_class: PasskeyClass,
1002) {
1003 let pk_reg = match pk_class {
1004 PasskeyClass::Any => {
1005 match client
1006 .idm_account_credential_update_passkey_init(session_token)
1007 .await
1008 {
1009 Ok(CUStatus {
1010 mfaregstate: CURegState::Passkey(pk_reg),
1011 ..
1012 }) => pk_reg,
1013 Ok(status) => {
1014 debug!(?status);
1015 eprintln!("An error occurred -> InvalidState");
1016 return;
1017 }
1018 Err(e) => {
1019 eprintln!("An error occurred -> {e:?}");
1020 return;
1021 }
1022 }
1023 }
1024 PasskeyClass::Attested => {
1025 match client
1026 .idm_account_credential_update_attested_passkey_init(session_token)
1027 .await
1028 {
1029 Ok(CUStatus {
1030 mfaregstate: CURegState::AttestedPasskey(pk_reg),
1031 ..
1032 }) => pk_reg,
1033 Ok(status) => {
1034 debug!(?status);
1035 eprintln!("An error occurred -> InvalidState");
1036 return;
1037 }
1038 Err(e) => {
1039 eprintln!("An error occurred -> {e:?}");
1040 return;
1041 }
1042 }
1043 }
1044 };
1045
1046 let mut wa = get_authenticator();
1049
1050 eprintln!("Your authenticator will now flash for you to interact with.");
1051 eprintln!("You may be asked to enter the PIN for your device.");
1052
1053 let rego = match wa.do_registration(client.get_origin().clone(), pk_reg) {
1054 Ok(rego) => rego,
1055 Err(e) => {
1056 error!("Error Signing -> {:?}", e);
1057 return;
1058 }
1059 };
1060
1061 let label: String = Input::new()
1062 .with_prompt("\nEnter a label for this Passkey # ")
1063 .allow_empty(false)
1064 .interact_text()
1065 .expect("Failed to interact with interactive session");
1066
1067 match pk_class {
1068 PasskeyClass::Any => {
1069 match client
1070 .idm_account_credential_update_passkey_finish(session_token, label, rego)
1071 .await
1072 {
1073 Ok(_) => println!("success"),
1074 Err(e) => {
1075 eprintln!("An error occurred -> {e:?}");
1076 }
1077 }
1078 }
1079 PasskeyClass::Attested => {
1080 match client
1081 .idm_account_credential_update_attested_passkey_finish(session_token, label, rego)
1082 .await
1083 {
1084 Ok(_) => println!("success"),
1085 Err(e) => {
1086 eprintln!("An error occurred -> {e:?}");
1087 }
1088 }
1089 }
1090 }
1091}
1092
1093async fn passkey_remove_prompt(
1094 session_token: &CUSessionToken,
1095 client: &KanidmClient,
1096 pk_class: PasskeyClass,
1097) {
1098 match client
1100 .idm_account_credential_update_status(session_token)
1101 .await
1102 {
1103 Ok(status) => match pk_class {
1104 PasskeyClass::Any => {
1105 if status.passkeys.is_empty() {
1106 println!("No passkeys are configured for this user");
1107 return;
1108 }
1109 println!("Current passkeys:");
1110 for pk in status.passkeys {
1111 println!(" {} ({})", pk.tag, pk.uuid);
1112 }
1113 }
1114 PasskeyClass::Attested => {
1115 if status.attested_passkeys.is_empty() {
1116 println!("No attested passkeys are configured for this user");
1117 return;
1118 }
1119 println!("Current attested passkeys:");
1120 for pk in status.attested_passkeys {
1121 println!(" {} ({})", pk.tag, pk.uuid);
1122 }
1123 }
1124 },
1125 Err(e) => {
1126 eprintln!("An error occurred retrieving existing credentials -> {e:?}");
1127 }
1128 }
1129
1130 let uuid_s: String = Input::new()
1131 .with_prompt("\nEnter the UUID of the Passkey to remove (blank to stop) # ")
1132 .validate_with(|input: &String| -> Result<(), &str> {
1133 if input.is_empty() || Uuid::parse_str(input).is_ok() {
1134 Ok(())
1135 } else {
1136 Err("This is not a valid UUID")
1137 }
1138 })
1139 .allow_empty(true)
1140 .interact_text()
1141 .expect("Failed to interact with interactive session");
1142
1143 if let Ok(uuid) = Uuid::parse_str(&uuid_s) {
1145 let result = match pk_class {
1146 PasskeyClass::Any => {
1147 client
1148 .idm_account_credential_update_passkey_remove(session_token, uuid)
1149 .await
1150 }
1151 PasskeyClass::Attested => {
1152 client
1153 .idm_account_credential_update_attested_passkey_remove(session_token, uuid)
1154 .await
1155 }
1156 };
1157
1158 if let Err(e) = result {
1159 eprintln!("An error occurred -> {e:?}");
1160 } else {
1161 println!("success");
1162 }
1163 } else {
1164 println!("{pk_class}s were NOT changed");
1165 }
1166}
1167
1168async fn sshkey_add_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
1169 let ssh_pub_key_str: String = Input::new()
1171 .with_prompt("\nEnter the SSH Public Key (blank to stop) # ")
1172 .validate_with(|input: &String| -> Result<(), &str> {
1173 if input.is_empty() || SshPublicKey::from_string(input).is_ok() {
1174 Ok(())
1175 } else {
1176 Err("This is not a valid SSH Public Key")
1177 }
1178 })
1179 .allow_empty(true)
1180 .interact_text()
1181 .expect("Failed to interact with interactive session");
1182
1183 if ssh_pub_key_str.is_empty() {
1184 println!("SSH Public Key was not added");
1185 return;
1186 }
1187
1188 let ssh_pub_key = match SshPublicKey::from_string(&ssh_pub_key_str) {
1189 Ok(spk) => spk,
1190 Err(_err) => {
1191 eprintln!("Failed to parse ssh public key that previously parsed correctly.");
1192 return;
1193 }
1194 };
1195
1196 let default_label = ssh_pub_key
1197 .comment
1198 .clone()
1199 .unwrap_or_else(|| ssh_pub_key.fingerprint().hash);
1200
1201 loop {
1202 let label: String = Input::new()
1204 .with_prompt("\nEnter the label of the new SSH Public Key")
1205 .default(default_label.clone())
1206 .interact_text()
1207 .expect("Failed to interact with interactive session");
1208
1209 if let Err(err) = client
1210 .idm_account_credential_update_sshkey_add(session_token, label, ssh_pub_key.clone())
1211 .await
1212 {
1213 match err {
1214 ClientErrorHttp(_, Some(InvalidLabel), _) => {
1215 eprintln!("Invalid SSH Public Key label - must only contain letters, numbers, and the characters '@' or '.'");
1216 continue;
1217 }
1218 ClientErrorHttp(_, Some(DuplicateLabel), _) => {
1219 eprintln!("SSH Public Key label already exists - choose another");
1220 continue;
1221 }
1222 ClientErrorHttp(_, Some(DuplicateKey), _) => {
1223 eprintln!("SSH Public Key already exists in this account");
1224 }
1225 _ => eprintln!("An error occurred -> {err:?}"),
1226 }
1227 break;
1228 } else {
1229 println!("Successfully added SSH Public Key");
1230 break;
1231 }
1232 }
1233}
1234
1235async fn sshkey_remove_prompt(session_token: &CUSessionToken, client: &KanidmClient) {
1236 let label: String = Input::new()
1237 .with_prompt("\nEnter the label of the new SSH Public Key (blank to stop) # ")
1238 .allow_empty(true)
1239 .interact_text()
1240 .expect("Failed to interact with interactive session");
1241
1242 if label.is_empty() {
1243 println!("SSH Public Key was NOT removed");
1244 return;
1245 }
1246
1247 if let Err(err) = client
1248 .idm_account_credential_update_sshkey_remove(session_token, label)
1249 .await
1250 {
1251 match err {
1252 ClientErrorHttp(_, Some(NoMatchingEntries), _) => {
1253 eprintln!("SSH Public Key does not exist. Keys were NOT removed.");
1254 }
1255 _ => eprintln!("An error occurred -> {err:?}"),
1256 }
1257 } else {
1258 println!("Successfully removed SSH Public Key");
1259 }
1260}
1261
1262fn display_warnings(warnings: &[CURegWarning]) {
1263 if !warnings.is_empty() {
1264 println!("Warnings:");
1265 }
1266 for warning in warnings {
1267 print!(" ⚠️ ");
1268 match warning {
1269 CURegWarning::MfaRequired => {
1270 println!("Multi-factor authentication required - add TOTP or replace your password with more secure method.");
1271 }
1272 CURegWarning::PasskeyRequired => {
1273 println!("Passkeys required");
1274 }
1275 CURegWarning::AttestedPasskeyRequired => {
1276 println!("Attested Passkeys required");
1277 }
1278 CURegWarning::AttestedResidentKeyRequired => {
1279 println!("Attested Resident Keys required");
1280 }
1281 CURegWarning::WebauthnAttestationUnsatisfiable => {
1282 println!("Attestation is unsatisfiable. Contact your administrator.");
1283 }
1284 CURegWarning::Unsatisfiable => {
1285 println!("Account policy is unsatisfiable. Contact your administrator.");
1286 }
1287 CURegWarning::WebauthnUserVerificationRequired => {
1288 println!(
1289 "The passkey you attempted to register did not provide user verification, please ensure a PIN or equivalent is set."
1290 );
1291 }
1292 CURegWarning::NoValidCredentials => {
1293 println!("Your account has no valid authentication registered - please create at least one credential to proceed.");
1294 }
1295 }
1296 }
1297}
1298
1299fn display_status(status: CUStatus) {
1300 let CUStatus {
1301 spn,
1302 displayname,
1303 ext_cred_portal,
1304 mfaregstate: _,
1305 can_commit,
1306 warnings,
1307 primary,
1308 primary_state,
1309 passkeys,
1310 passkeys_state,
1311 attested_passkeys,
1312 attested_passkeys_state,
1313 attested_passkeys_allowed_devices,
1314 unixcred,
1315 unixcred_state,
1316 sshkeys,
1317 sshkeys_state,
1318 } = status;
1319
1320 println!("spn: {spn}");
1321 println!("Name: {displayname}");
1322
1323 match ext_cred_portal {
1324 CUExtPortal::None => {}
1325 CUExtPortal::Hidden => {
1326 println!("Externally Managed: Not all features may be available");
1327 println!(" Contact your admin for more details.");
1328 }
1329 CUExtPortal::Some(url) => {
1330 println!("Externally Managed: Not all features may be available");
1331 println!(" Visit {} to update your account details.", url.as_str());
1332 }
1333 };
1334
1335 println!("Primary Credential:");
1336
1337 match primary_state {
1338 CUCredState::Modifiable => {
1339 if let Some(cred_detail) = &primary {
1340 print!("{cred_detail}");
1341 } else {
1342 println!(" not set");
1343 }
1344 }
1345 CUCredState::DeleteOnly => {
1346 if let Some(cred_detail) = &primary {
1347 print!("{cred_detail}");
1348 } else {
1349 println!(" unable to modify - access denied");
1350 }
1351 }
1352 CUCredState::AccessDeny => {
1353 println!(" unable to modify - access denied");
1354 }
1355 CUCredState::PolicyDeny => {
1356 println!(" unable to modify - account policy denied");
1357 }
1358 }
1359
1360 println!("Passkeys:");
1361 match passkeys_state {
1362 CUCredState::Modifiable => {
1363 if passkeys.is_empty() {
1364 println!(" not set");
1365 } else {
1366 for pk in passkeys {
1367 println!(" {} ({})", pk.tag, pk.uuid);
1368 }
1369 }
1370 }
1371 CUCredState::DeleteOnly => {
1372 if passkeys.is_empty() {
1373 println!(" unable to modify - access denied");
1374 } else {
1375 for pk in passkeys {
1376 println!(" {} ({})", pk.tag, pk.uuid);
1377 }
1378 }
1379 }
1380 CUCredState::AccessDeny => {
1381 println!(" unable to modify - access denied");
1382 }
1383 CUCredState::PolicyDeny => {
1384 println!(" unable to modify - account policy denied");
1385 }
1386 }
1387
1388 println!("Attested Passkeys:");
1389 match attested_passkeys_state {
1390 CUCredState::Modifiable => {
1391 if attested_passkeys.is_empty() {
1392 println!(" not set");
1393 } else {
1394 for pk in attested_passkeys {
1395 println!(" {} ({})", pk.tag, pk.uuid);
1396 }
1397 }
1398
1399 println!(" --");
1400 println!(" The following devices models are allowed by account policy");
1401 for dev in attested_passkeys_allowed_devices {
1402 println!(" - {dev}");
1403 }
1404 }
1405 CUCredState::DeleteOnly => {
1406 if attested_passkeys.is_empty() {
1407 println!(" unable to modify - attestation policy not configured");
1408 } else {
1409 for pk in attested_passkeys {
1410 println!(" {} ({})", pk.tag, pk.uuid);
1411 }
1412 }
1413 }
1414 CUCredState::AccessDeny => {
1415 println!(" unable to modify - access denied");
1416 }
1417 CUCredState::PolicyDeny => {
1418 println!(" unable to modify - attestation policy not configured");
1419 }
1420 }
1421
1422 println!("Unix (sudo) Password:");
1423 match unixcred_state {
1424 CUCredState::Modifiable => {
1425 if let Some(cred_detail) = &unixcred {
1426 print!("{cred_detail}");
1427 } else {
1428 println!(" not set");
1429 }
1430 }
1431 CUCredState::DeleteOnly => {
1432 if let Some(cred_detail) = &unixcred {
1433 print!("{cred_detail}");
1434 } else {
1435 println!(" unable to modify - access denied");
1436 }
1437 }
1438 CUCredState::AccessDeny => {
1439 println!(" unable to modify - access denied");
1440 }
1441 CUCredState::PolicyDeny => {
1442 println!(" unable to modify - account does not have posix attributes");
1443 }
1444 }
1445
1446 println!("SSH Public Keys:");
1447 match sshkeys_state {
1448 CUCredState::Modifiable => {
1449 if sshkeys.is_empty() {
1450 println!(" not set");
1451 } else {
1452 for (label, sk) in sshkeys {
1453 println!(" {label}: {sk}");
1454 }
1455 }
1456 }
1457 CUCredState::DeleteOnly => {
1458 if sshkeys.is_empty() {
1459 println!(" unable to modify - access denied");
1460 } else {
1461 for (label, sk) in sshkeys {
1462 println!(" {label}: {sk}");
1463 }
1464 }
1465 }
1466 CUCredState::AccessDeny => {
1467 println!(" unable to modify - access denied");
1468 }
1469 CUCredState::PolicyDeny => {
1470 println!(" unable to modify - account policy denied");
1471 }
1472 }
1473
1474 display_warnings(&warnings);
1478
1479 println!("Can Commit: {can_commit}");
1480}
1481
1482async fn credential_update_exec(
1484 session_token: CUSessionToken,
1485 status: CUStatus,
1486 client: KanidmClient,
1487) {
1488 trace!("started credential update exec");
1489 display_status(status);
1491 loop {
1493 let input: String = Input::new()
1495 .with_prompt("\ncred update (? for help) # ")
1496 .validate_with(|input: &String| -> Result<(), &str> {
1497 if CUAction::from_str(input).is_ok() {
1498 Ok(())
1499 } else {
1500 Err("This is not a valid command. See help for valid options (?)")
1501 }
1502 })
1503 .interact_text()
1504 .expect("Failed to interact with interactive session");
1505
1506 let action = match CUAction::from_str(&input) {
1508 Ok(a) => a,
1509 Err(_) => continue,
1510 };
1511
1512 trace!(?action);
1513
1514 match action {
1515 CUAction::Help => {
1516 print!("{action}");
1517 }
1518 CUAction::Status => {
1519 match client
1520 .idm_account_credential_update_status(&session_token)
1521 .await
1522 {
1523 Ok(status) => display_status(status),
1524 Err(e) => {
1525 eprintln!("An error occurred -> {e:?}");
1526 }
1527 }
1528 }
1529 CUAction::Password => {
1530 let password_a = Password::new()
1531 .with_prompt("New password")
1532 .interact()
1533 .expect("Failed to interact with interactive session");
1534 let password_b = Password::new()
1535 .with_prompt("Confirm password")
1536 .interact()
1537 .expect("Failed to interact with interactive session");
1538
1539 if password_a != password_b {
1540 eprintln!("Passwords do not match");
1541 } else if let Err(e) = client
1542 .idm_account_credential_update_set_password(&session_token, &password_a)
1543 .await
1544 {
1545 match e {
1546 ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
1547 eprintln!("Password was not secure enough, please consider the following suggestions:");
1548 for fb_item in feedback.iter() {
1549 eprintln!(" - {fb_item}")
1550 }
1551 }
1552 _ => eprintln!("An error occurred -> {e:?}"),
1553 }
1554 } else {
1555 println!("Successfully reset password.");
1556 }
1557 }
1558 CUAction::Totp => totp_enrol_prompt(&session_token, &client).await,
1559 CUAction::TotpRemove => {
1560 match client
1561 .idm_account_credential_update_status(&session_token)
1562 .await
1563 {
1564 Ok(status) => match status.primary {
1565 Some(CredentialDetail {
1566 uuid: _,
1567 type_: CredentialDetailType::PasswordMfa(totp_labels, ..),
1568 }) => {
1569 if totp_labels.is_empty() {
1570 println!("No totps are configured for this user");
1571 return;
1572 } else {
1573 println!("Current totps:");
1574 for totp_label in totp_labels {
1575 println!(" {totp_label}");
1576 }
1577 }
1578 }
1579 _ => {
1580 println!("No totps are configured for this user");
1581 return;
1582 }
1583 },
1584 Err(e) => {
1585 eprintln!("An error occurred retrieving existing credentials -> {e:?}");
1586 }
1587 }
1588
1589 let label: String = Input::new()
1590 .with_prompt("\nEnter the label of the Passkey to remove (blank to stop) # ")
1591 .allow_empty(true)
1592 .interact_text()
1593 .expect("Failed to interact with interactive session");
1594
1595 if !label.is_empty() {
1596 if let Err(e) = client
1597 .idm_account_credential_update_remove_totp(&session_token, &label)
1598 .await
1599 {
1600 eprintln!("An error occurred -> {e:?}");
1601 } else {
1602 println!("success");
1603 }
1604 } else {
1605 println!("Totp was NOT removed");
1606 }
1607 }
1608 CUAction::BackupCodes => {
1609 match client
1610 .idm_account_credential_update_backup_codes_generate(&session_token)
1611 .await
1612 {
1613 Ok(CUStatus {
1614 mfaregstate: CURegState::BackupCodes(codes),
1615 ..
1616 }) => {
1617 println!("Please store these Backup codes in a safe place");
1618 println!("They will only be displayed ONCE");
1619 for code in codes {
1620 println!(" {code}")
1621 }
1622 }
1623 Ok(status) => {
1624 debug!(?status);
1625 eprintln!("An error occurred -> InvalidState");
1626 }
1627 Err(e) => {
1628 eprintln!("An error occurred -> {e:?}");
1629 }
1630 }
1631 }
1632 CUAction::Remove => {
1633 if Confirm::new()
1634 .with_prompt("Do you want to remove your primary credential?")
1635 .interact()
1636 .expect("Failed to interact with interactive session")
1637 {
1638 if let Err(e) = client
1639 .idm_account_credential_update_primary_remove(&session_token)
1640 .await
1641 {
1642 eprintln!("An error occurred -> {e:?}");
1643 } else {
1644 println!("success");
1645 }
1646 } else {
1647 println!("Primary credential was NOT removed");
1648 }
1649 }
1650 CUAction::Passkey => {
1651 passkey_enrol_prompt(&session_token, &client, PasskeyClass::Any).await
1652 }
1653 CUAction::PasskeyRemove => {
1654 passkey_remove_prompt(&session_token, &client, PasskeyClass::Any).await
1655 }
1656 CUAction::AttestedPasskey => {
1657 passkey_enrol_prompt(&session_token, &client, PasskeyClass::Attested).await
1658 }
1659 CUAction::AttestedPasskeyRemove => {
1660 passkey_remove_prompt(&session_token, &client, PasskeyClass::Attested).await
1661 }
1662
1663 CUAction::UnixPassword => {
1664 let password_a = Password::new()
1665 .with_prompt("New Unix Password")
1666 .interact()
1667 .expect("Failed to interact with interactive session");
1668 let password_b = Password::new()
1669 .with_prompt("Confirm password")
1670 .interact()
1671 .expect("Failed to interact with interactive session");
1672
1673 if password_a != password_b {
1674 eprintln!("Passwords do not match");
1675 } else if let Err(e) = client
1676 .idm_account_credential_update_set_unix_password(&session_token, &password_a)
1677 .await
1678 {
1679 match e {
1680 ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
1681 eprintln!("Password was not secure enough, please consider the following suggestions:");
1682 for fb_item in feedback.iter() {
1683 eprintln!(" - {fb_item}")
1684 }
1685 }
1686 _ => eprintln!("An error occurred -> {e:?}"),
1687 }
1688 } else {
1689 println!("Successfully reset unix password.");
1690 }
1691 }
1692
1693 CUAction::UnixPasswordRemove => {
1694 if Confirm::new()
1695 .with_prompt("Do you want to remove your unix password?")
1696 .interact()
1697 .expect("Failed to interact with interactive session")
1698 {
1699 if let Err(e) = client
1700 .idm_account_credential_update_unix_remove(&session_token)
1701 .await
1702 {
1703 eprintln!("An error occurred -> {e:?}");
1704 } else {
1705 println!("success");
1706 }
1707 } else {
1708 println!("unix password was NOT removed");
1709 }
1710 }
1711 CUAction::SshPublicKey => sshkey_add_prompt(&session_token, &client).await,
1712 CUAction::SshPublicKeyRemove => sshkey_remove_prompt(&session_token, &client).await,
1713 CUAction::End => {
1714 println!("Changes were NOT saved.");
1715 break;
1716 }
1717 CUAction::Commit => {
1718 match client
1719 .idm_account_credential_update_status(&session_token)
1720 .await
1721 {
1722 Ok(status) => {
1723 if !status.can_commit {
1724 display_warnings(&status.warnings);
1725 println!("Changes have NOT been saved.");
1727 continue;
1728 }
1729 }
1731 Err(e) => {
1732 eprintln!("An error occurred -> {e:?}");
1733 }
1734 }
1735
1736 if Confirm::new()
1737 .with_prompt("Do you want to commit your changes?")
1738 .interact()
1739 .expect("Failed to interact with interactive session")
1740 {
1741 if let Err(e) = client
1742 .idm_account_credential_update_commit(&session_token)
1743 .await
1744 {
1745 eprintln!("An error occurred -> {e:?}");
1746 println!("Changes have NOT been saved.");
1747 } else {
1748 println!("Success - Changes have been saved.");
1749 break;
1750 }
1751 } else {
1752 println!("Changes have NOT been saved.");
1753 }
1754 }
1755 }
1756 }
1757 trace!("ended credential update exec");
1758}