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