kanidm_cli/
person.rs

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