kanidm_cli/
person.rs

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