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