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