kanidm_cli/
person.rs

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