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