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