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