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