Skip to main content

kanidm_cli/
person.rs

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