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