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        }
1245    }
1246}
1247
1248fn display_status(status: CUStatus) {
1249    let CUStatus {
1250        spn,
1251        displayname,
1252        ext_cred_portal,
1253        mfaregstate: _,
1254        can_commit,
1255        warnings,
1256        primary,
1257        primary_state,
1258        passkeys,
1259        passkeys_state,
1260        attested_passkeys,
1261        attested_passkeys_state,
1262        attested_passkeys_allowed_devices,
1263        unixcred,
1264        unixcred_state,
1265        sshkeys,
1266        sshkeys_state,
1267    } = status;
1268
1269    println!("spn: {spn}");
1270    println!("Name: {displayname}");
1271
1272    match ext_cred_portal {
1273        CUExtPortal::None => {}
1274        CUExtPortal::Hidden => {
1275            println!("Externally Managed: Not all features may be available");
1276            println!("    Contact your admin for more details.");
1277        }
1278        CUExtPortal::Some(url) => {
1279            println!("Externally Managed: Not all features may be available");
1280            println!("    Visit {} to update your account details.", url.as_str());
1281        }
1282    };
1283
1284    println!("Primary Credential:");
1285
1286    match primary_state {
1287        CUCredState::Modifiable => {
1288            if let Some(cred_detail) = &primary {
1289                print!("{cred_detail}");
1290            } else {
1291                println!("  not set");
1292            }
1293        }
1294        CUCredState::DeleteOnly => {
1295            if let Some(cred_detail) = &primary {
1296                print!("{cred_detail}");
1297            } else {
1298                println!("  unable to modify - access denied");
1299            }
1300        }
1301        CUCredState::AccessDeny => {
1302            println!("  unable to modify - access denied");
1303        }
1304        CUCredState::PolicyDeny => {
1305            println!("  unable to modify - account policy denied");
1306        }
1307    }
1308
1309    println!("Passkeys:");
1310    match passkeys_state {
1311        CUCredState::Modifiable => {
1312            if passkeys.is_empty() {
1313                println!("  not set");
1314            } else {
1315                for pk in passkeys {
1316                    println!("  {} ({})", pk.tag, pk.uuid);
1317                }
1318            }
1319        }
1320        CUCredState::DeleteOnly => {
1321            if passkeys.is_empty() {
1322                println!("  unable to modify - access denied");
1323            } else {
1324                for pk in passkeys {
1325                    println!("  {} ({})", pk.tag, pk.uuid);
1326                }
1327            }
1328        }
1329        CUCredState::AccessDeny => {
1330            println!("  unable to modify - access denied");
1331        }
1332        CUCredState::PolicyDeny => {
1333            println!("  unable to modify - account policy denied");
1334        }
1335    }
1336
1337    println!("Attested Passkeys:");
1338    match attested_passkeys_state {
1339        CUCredState::Modifiable => {
1340            if attested_passkeys.is_empty() {
1341                println!("  not set");
1342            } else {
1343                for pk in attested_passkeys {
1344                    println!("  {} ({})", pk.tag, pk.uuid);
1345                }
1346            }
1347
1348            println!("  --");
1349            println!("  The following devices models are allowed by account policy");
1350            for dev in attested_passkeys_allowed_devices {
1351                println!("  - {dev}");
1352            }
1353        }
1354        CUCredState::DeleteOnly => {
1355            if attested_passkeys.is_empty() {
1356                println!("  unable to modify - attestation policy not configured");
1357            } else {
1358                for pk in attested_passkeys {
1359                    println!("  {} ({})", pk.tag, pk.uuid);
1360                }
1361            }
1362        }
1363        CUCredState::AccessDeny => {
1364            println!("  unable to modify - access denied");
1365        }
1366        CUCredState::PolicyDeny => {
1367            println!("  unable to modify - attestation policy not configured");
1368        }
1369    }
1370
1371    println!("Unix (sudo) Password:");
1372    match unixcred_state {
1373        CUCredState::Modifiable => {
1374            if let Some(cred_detail) = &unixcred {
1375                print!("{cred_detail}");
1376            } else {
1377                println!("  not set");
1378            }
1379        }
1380        CUCredState::DeleteOnly => {
1381            if let Some(cred_detail) = &unixcred {
1382                print!("{cred_detail}");
1383            } else {
1384                println!("  unable to modify - access denied");
1385            }
1386        }
1387        CUCredState::AccessDeny => {
1388            println!("  unable to modify - access denied");
1389        }
1390        CUCredState::PolicyDeny => {
1391            println!("  unable to modify - account does not have posix attributes");
1392        }
1393    }
1394
1395    println!("SSH Public Keys:");
1396    match sshkeys_state {
1397        CUCredState::Modifiable => {
1398            if sshkeys.is_empty() {
1399                println!("  not set");
1400            } else {
1401                for (label, sk) in sshkeys {
1402                    println!("  {label}: {sk}");
1403                }
1404            }
1405        }
1406        CUCredState::DeleteOnly => {
1407            if sshkeys.is_empty() {
1408                println!("  unable to modify - access denied");
1409            } else {
1410                for (label, sk) in sshkeys {
1411                    println!("  {label}: {sk}");
1412                }
1413            }
1414        }
1415        CUCredState::AccessDeny => {
1416            println!("  unable to modify - access denied");
1417        }
1418        CUCredState::PolicyDeny => {
1419            println!("  unable to modify - account policy denied");
1420        }
1421    }
1422
1423    // We may need to be able to display if there are dangling
1424    // curegstates, but the cli ui statemachine can match the
1425    // server so it may not be needed?
1426    display_warnings(&warnings);
1427
1428    println!("Can Commit: {can_commit}");
1429}
1430
1431/// This is the REPL for updating a credential for a given account
1432async fn credential_update_exec(
1433    session_token: CUSessionToken,
1434    status: CUStatus,
1435    client: KanidmClient,
1436) {
1437    trace!("started credential update exec");
1438    // Show the initial status,
1439    display_status(status);
1440    // Setup to work
1441    loop {
1442        // Display Prompt
1443        let input: String = Input::new()
1444            .with_prompt("\ncred update (? for help) # ")
1445            .validate_with(|input: &String| -> Result<(), &str> {
1446                if CUAction::from_str(input).is_ok() {
1447                    Ok(())
1448                } else {
1449                    Err("This is not a valid command. See help for valid options (?)")
1450                }
1451            })
1452            .interact_text()
1453            .expect("Failed to interact with interactive session");
1454
1455        // Get action
1456        let action = match CUAction::from_str(&input) {
1457            Ok(a) => a,
1458            Err(_) => continue,
1459        };
1460
1461        trace!(?action);
1462
1463        match action {
1464            CUAction::Help => {
1465                print!("{action}");
1466            }
1467            CUAction::Status => {
1468                match client
1469                    .idm_account_credential_update_status(&session_token)
1470                    .await
1471                {
1472                    Ok(status) => display_status(status),
1473                    Err(e) => {
1474                        eprintln!("An error occurred -> {e:?}");
1475                    }
1476                }
1477            }
1478            CUAction::Password => {
1479                let password_a = Password::new()
1480                    .with_prompt("New password")
1481                    .interact()
1482                    .expect("Failed to interact with interactive session");
1483                let password_b = Password::new()
1484                    .with_prompt("Confirm password")
1485                    .interact()
1486                    .expect("Failed to interact with interactive session");
1487
1488                if password_a != password_b {
1489                    eprintln!("Passwords do not match");
1490                } else if let Err(e) = client
1491                    .idm_account_credential_update_set_password(&session_token, &password_a)
1492                    .await
1493                {
1494                    match e {
1495                        ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
1496                            eprintln!("Password was not secure enough, please consider the following suggestions:");
1497                            for fb_item in feedback.iter() {
1498                                eprintln!(" - {fb_item}")
1499                            }
1500                        }
1501                        _ => eprintln!("An error occurred -> {e:?}"),
1502                    }
1503                } else {
1504                    println!("Successfully reset password.");
1505                }
1506            }
1507            CUAction::Totp => totp_enroll_prompt(&session_token, &client).await,
1508            CUAction::TotpRemove => {
1509                match client
1510                    .idm_account_credential_update_status(&session_token)
1511                    .await
1512                {
1513                    Ok(status) => match status.primary {
1514                        Some(CredentialDetail {
1515                            uuid: _,
1516                            type_: CredentialDetailType::PasswordMfa(totp_labels, ..),
1517                        }) => {
1518                            if totp_labels.is_empty() {
1519                                println!("No totps are configured for this user");
1520                                return;
1521                            } else {
1522                                println!("Current totps:");
1523                                for totp_label in totp_labels {
1524                                    println!("  {totp_label}");
1525                                }
1526                            }
1527                        }
1528                        _ => {
1529                            println!("No totps are configured for this user");
1530                            return;
1531                        }
1532                    },
1533                    Err(e) => {
1534                        eprintln!("An error occurred retrieving existing credentials -> {e:?}");
1535                    }
1536                }
1537
1538                let label: String = Input::new()
1539                    .with_prompt("\nEnter the label of the Passkey to remove (blank to stop) # ")
1540                    .allow_empty(true)
1541                    .interact_text()
1542                    .expect("Failed to interact with interactive session");
1543
1544                if !label.is_empty() {
1545                    if let Err(e) = client
1546                        .idm_account_credential_update_remove_totp(&session_token, &label)
1547                        .await
1548                    {
1549                        eprintln!("An error occurred -> {e:?}");
1550                    } else {
1551                        println!("success");
1552                    }
1553                } else {
1554                    println!("Totp was NOT removed");
1555                }
1556            }
1557            CUAction::BackupCodes => {
1558                match client
1559                    .idm_account_credential_update_backup_codes_generate(&session_token)
1560                    .await
1561                {
1562                    Ok(CUStatus {
1563                        mfaregstate: CURegState::BackupCodes(codes),
1564                        ..
1565                    }) => {
1566                        println!("Please store these Backup codes in a safe place");
1567                        println!("They will only be displayed ONCE");
1568                        for code in codes {
1569                            println!("  {code}")
1570                        }
1571                    }
1572                    Ok(status) => {
1573                        debug!(?status);
1574                        eprintln!("An error occurred -> InvalidState");
1575                    }
1576                    Err(e) => {
1577                        eprintln!("An error occurred -> {e:?}");
1578                    }
1579                }
1580            }
1581            CUAction::Remove => {
1582                if Confirm::new()
1583                    .with_prompt("Do you want to remove your primary credential?")
1584                    .interact()
1585                    .expect("Failed to interact with interactive session")
1586                {
1587                    if let Err(e) = client
1588                        .idm_account_credential_update_primary_remove(&session_token)
1589                        .await
1590                    {
1591                        eprintln!("An error occurred -> {e:?}");
1592                    } else {
1593                        println!("success");
1594                    }
1595                } else {
1596                    println!("Primary credential was NOT removed");
1597                }
1598            }
1599            CUAction::Passkey => {
1600                passkey_enroll_prompt(&session_token, &client, PasskeyClass::Any).await
1601            }
1602            CUAction::PasskeyRemove => {
1603                passkey_remove_prompt(&session_token, &client, PasskeyClass::Any).await
1604            }
1605            CUAction::AttestedPasskey => {
1606                passkey_enroll_prompt(&session_token, &client, PasskeyClass::Attested).await
1607            }
1608            CUAction::AttestedPasskeyRemove => {
1609                passkey_remove_prompt(&session_token, &client, PasskeyClass::Attested).await
1610            }
1611
1612            CUAction::UnixPassword => {
1613                let password_a = Password::new()
1614                    .with_prompt("New Unix Password")
1615                    .interact()
1616                    .expect("Failed to interact with interactive session");
1617                let password_b = Password::new()
1618                    .with_prompt("Confirm password")
1619                    .interact()
1620                    .expect("Failed to interact with interactive session");
1621
1622                if password_a != password_b {
1623                    eprintln!("Passwords do not match");
1624                } else if let Err(e) = client
1625                    .idm_account_credential_update_set_unix_password(&session_token, &password_a)
1626                    .await
1627                {
1628                    match e {
1629                        ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
1630                            eprintln!("Password was not secure enough, please consider the following suggestions:");
1631                            for fb_item in feedback.iter() {
1632                                eprintln!(" - {fb_item}")
1633                            }
1634                        }
1635                        _ => eprintln!("An error occurred -> {e:?}"),
1636                    }
1637                } else {
1638                    println!("Successfully reset unix password.");
1639                }
1640            }
1641
1642            CUAction::UnixPasswordRemove => {
1643                if Confirm::new()
1644                    .with_prompt("Do you want to remove your unix password?")
1645                    .interact()
1646                    .expect("Failed to interact with interactive session")
1647                {
1648                    if let Err(e) = client
1649                        .idm_account_credential_update_unix_remove(&session_token)
1650                        .await
1651                    {
1652                        eprintln!("An error occurred -> {e:?}");
1653                    } else {
1654                        println!("success");
1655                    }
1656                } else {
1657                    println!("unix password was NOT removed");
1658                }
1659            }
1660            CUAction::SshPublicKey => sshkey_add_prompt(&session_token, &client).await,
1661            CUAction::SshPublicKeyRemove => sshkey_remove_prompt(&session_token, &client).await,
1662            CUAction::End => {
1663                println!("Changes were NOT saved.");
1664                break;
1665            }
1666            CUAction::Commit => {
1667                match client
1668                    .idm_account_credential_update_status(&session_token)
1669                    .await
1670                {
1671                    Ok(status) => {
1672                        if !status.can_commit {
1673                            display_warnings(&status.warnings);
1674                            // Reset the loop
1675                            println!("Changes have NOT been saved.");
1676                            continue;
1677                        }
1678                        // Can proceed
1679                    }
1680                    Err(e) => {
1681                        eprintln!("An error occurred -> {e:?}");
1682                    }
1683                }
1684
1685                if Confirm::new()
1686                    .with_prompt("Do you want to commit your changes?")
1687                    .interact()
1688                    .expect("Failed to interact with interactive session")
1689                {
1690                    if let Err(e) = client
1691                        .idm_account_credential_update_commit(&session_token)
1692                        .await
1693                    {
1694                        eprintln!("An error occurred -> {e:?}");
1695                        println!("Changes have NOT been saved.");
1696                    } else {
1697                        println!("Success - Changes have been saved.");
1698                        break;
1699                    }
1700                } else {
1701                    println!("Changes have NOT been saved.");
1702                }
1703            }
1704        }
1705    }
1706    trace!("ended credential update exec");
1707}