kanidm_cli/
lib.rs

1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![deny(clippy::todo)]
4#![deny(clippy::unimplemented)]
5#![deny(clippy::unwrap_used)]
6#![deny(clippy::panic)]
7#![deny(clippy::unreachable)]
8#![deny(clippy::await_holding_lock)]
9#![deny(clippy::needless_pass_by_value)]
10#![deny(clippy::trivially_copy_pass_by_ref)]
11// We allow expect since it forces good error messages at the least.
12#![allow(clippy::expect_used)]
13#[macro_use]
14extern crate tracing;
15
16pub(crate) use crate::common::OpType;
17use std::path::PathBuf;
18
19use identify_user_no_tui::{run_identity_verification_no_tui, IdentifyUserState};
20
21use kanidm_client::{ClientError, StatusCode};
22use url::Url;
23use uuid::Uuid;
24
25include!("../opt/kanidm.rs");
26
27mod common;
28mod domain;
29mod graph;
30mod group;
31mod oauth2;
32mod person;
33mod raw;
34mod recycle;
35mod schema;
36mod serviceaccount;
37mod session;
38mod synch;
39mod system_config;
40mod webauthn;
41
42/// Throws an error and exits the program when we get an error
43pub(crate) fn handle_client_error(response: ClientError, _output_mode: OutputMode) {
44    match response {
45        ClientError::Http(status, error, opid) => {
46            let error_msg = match &error {
47                Some(msg) => format!(" {msg:?}"),
48                None => "".to_string(),
49            };
50            error!("OperationId: {:?}", opid);
51            if status == StatusCode::INTERNAL_SERVER_ERROR {
52                error!("Internal Server Error in response: {}", error_msg);
53                std::process::exit(1);
54            } else if status == StatusCode::NOT_FOUND {
55                error!("Item not found: Check all names are correct.");
56                std::process::exit(1);
57            } else {
58                error!("HTTP Error: {}{}", status, error_msg);
59                std::process::exit(1);
60            }
61        }
62        ClientError::Transport(e) => {
63            error!("HTTP-Transport Related Error: {:?}", e);
64            std::process::exit(1);
65        }
66        ClientError::UntrustedCertificate(e) => {
67            error!("Untrusted Certificate Error: {:?}", e);
68            std::process::exit(1);
69        }
70        _ => {
71            eprintln!("{response:?}");
72            std::process::exit(1);
73        }
74    };
75}
76
77pub(crate) fn handle_group_account_policy_error(response: ClientError, _output_mode: OutputMode) {
78    use kanidm_proto::internal::OperationError::SchemaViolation;
79    use kanidm_proto::internal::SchemaError::AttributeNotValidForClass;
80
81    if let ClientError::Http(_status, Some(SchemaViolation(AttributeNotValidForClass(att))), opid) =
82        response
83    {
84        error!("OperationId: {:?}", opid);
85        error!("Cannot update account-policy attribute {att}. Is account-policy enabled on this group?");
86    } else {
87        handle_client_error(response, _output_mode);
88    }
89}
90
91impl SelfOpt {
92    pub async fn exec(&self, opt: KanidmClientParser) {
93        match self {
94            SelfOpt::Whoami => {
95                let client = opt.to_client(OpType::Read).await;
96
97                match client.whoami().await {
98                    Ok(o_ent) => {
99                        match o_ent {
100                            Some(ent) => {
101                                opt.output_mode.print_message(ent);
102                            }
103                            None => {
104                                error!("Authentication with cached token failed, can't query information.");
105                                // TODO: remove token when we know it's not valid
106                            }
107                        }
108                    }
109                    Err(e) => handle_client_error(e, opt.output_mode),
110                }
111            }
112            SelfOpt::IdentifyUser => {
113                let client = opt.to_client(OpType::Write).await;
114                let whoami_response = match client.whoami().await {
115                    Ok(o_ent) => {
116                        match o_ent {
117                            Some(ent) => ent,
118                            None => {
119                                eprintln!("Authentication with cached token failed, can't query information."); // TODO: add an error ID (login, or clear token cache)
120                                return;
121                            }
122                        }
123                    }
124                    Err(e) => {
125                        opt.output_mode
126                            .print_message(format!("Error querying whoami endpoint: {e:?}")); // TODO: add an error ID (internal/web response error, restart or check connectivity)
127                        return;
128                    }
129                };
130
131                let spn =
132                    match whoami_response.attrs.get("spn").and_then(|v| v.first()) {
133                        Some(spn) => spn,
134                        None => {
135                            opt.output_mode.print_message("Failed to parse your SPN from the system's whoami endpoint, exiting!"); // TODO: add an error ID (internal/web response error, restart)
136                            return;
137                        }
138                    };
139
140                run_identity_verification_no_tui(IdentifyUserState::Start, client, spn, None).await;
141            } // end PersonOpt::Validity
142        }
143    }
144}
145
146impl SystemOpt {
147    pub async fn exec(&self, opt: KanidmClientParser) {
148        match self {
149            SystemOpt::Api { commands } => commands.exec(opt).await,
150            SystemOpt::PwBadlist { commands } => commands.exec(opt).await,
151            SystemOpt::DeniedNames { commands } => commands.exec(opt).await,
152            SystemOpt::Oauth2 { commands } => commands.exec(opt).await,
153            SystemOpt::Domain { commands } => commands.exec(opt).await,
154            SystemOpt::Message { commands } => commands.exec(opt).await,
155            SystemOpt::Synch { commands } => commands.exec(opt).await,
156        }
157    }
158}
159
160impl KanidmClientParser {
161    pub async fn exec(self) {
162        match self.commands.clone() {
163            KanidmClientOpt::Raw { commands } => commands.exec(self).await,
164            KanidmClientOpt::Login(lopt) => lopt.exec(self).await,
165            KanidmClientOpt::Reauth => self.reauth().await,
166            KanidmClientOpt::Logout(lopt) => lopt.exec(self).await,
167            KanidmClientOpt::Session { commands } => commands.exec(self).await,
168            KanidmClientOpt::CSelf { commands } => commands.exec(self).await,
169            KanidmClientOpt::Person { commands } => commands.exec(self).await,
170            KanidmClientOpt::ServiceAccount { commands } => commands.exec(self).await,
171            KanidmClientOpt::Group { commands } => commands.exec(self).await,
172            KanidmClientOpt::Graph(gops) => gops.exec(self).await,
173            KanidmClientOpt::System { commands } => commands.exec(self).await,
174            KanidmClientOpt::Schema {
175                commands: SchemaOpt::Class { commands },
176            } => commands.exec(self).await,
177            KanidmClientOpt::Schema {
178                commands: SchemaOpt::Attribute { commands },
179            } => commands.exec(self).await,
180            KanidmClientOpt::Recycle { commands } => commands.exec(self).await,
181            KanidmClientOpt::Version => {
182                self.output_mode
183                    .print_message(format!("kanidm {}", env!("KANIDM_PKG_VERSION")));
184            }
185        }
186    }
187}
188
189pub(crate) fn password_prompt(prompt: &str) -> Option<String> {
190    for _ in 0..3 {
191        let password = dialoguer::Password::new()
192            .with_prompt(prompt)
193            .interact()
194            .ok()?;
195
196        let password_confirm = dialoguer::Password::new()
197            .with_prompt("Reenter the password to confirm: ")
198            .interact()
199            .ok()?;
200
201        if password == password_confirm {
202            return Some(password);
203        } else {
204            error!("Passwords do not match");
205        }
206    }
207    None
208}
209
210pub const IDENTITY_UNAVAILABLE_ERROR_MESSAGE: &str = "Identity verification is not available.";
211pub const CODE_FAILURE_ERROR_MESSAGE: &str = "The provided verification code is invalid. Check the code with the other person, and if it remains invalid, they may be attempting to fool you!";
212pub const INVALID_STATE_ERROR_MESSAGE: &str =
213    "The user identification flow is in an invalid state 😵😵";
214
215mod identify_user_no_tui {
216    use crate::{
217        CODE_FAILURE_ERROR_MESSAGE, IDENTITY_UNAVAILABLE_ERROR_MESSAGE, INVALID_STATE_ERROR_MESSAGE,
218    };
219
220    use kanidm_client::{ClientError, KanidmClient};
221    use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
222
223    use dialoguer::{Confirm, Input};
224    use regex::Regex;
225    use std::{
226        io::{stdout, Write},
227        sync::LazyLock,
228        time::SystemTime,
229    };
230
231    pub static VALIDATE_TOTP_RE: LazyLock<Regex> = LazyLock::new(|| {
232        #[allow(clippy::expect_used)]
233        Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") // TODO: add an error ID (internal error, restart)
234    });
235
236    pub(super) enum IdentifyUserState {
237        Start,
238        IdDisplayAndSubmit,
239        SubmitCode,
240        DisplayCodeFirst { self_totp: u32, step: u32 },
241        DisplayCodeSecond { self_totp: u32, step: u32 },
242    }
243
244    fn server_error(e: &ClientError) {
245        eprintln!("Server error!"); // TODO: add an error ID (internal error, restart)
246        eprintln!("{e:?}");
247        println!("Exiting...");
248    }
249
250    pub(super) async fn run_identity_verification_no_tui(
251        mut state: IdentifyUserState,
252        client: KanidmClient,
253        self_id: &str,
254        mut other_id: Option<String>,
255    ) {
256        loop {
257            match state {
258                IdentifyUserState::Start => {
259                    let res = match &client
260                        .idm_person_identify_user(self_id, IdentifyUserRequest::Start)
261                        .await
262                    {
263                        Ok(res) => res.clone(),
264                        Err(e) => {
265                            return server_error(e);
266                        }
267                    };
268                    match res {
269                        IdentifyUserResponse::IdentityVerificationUnavailable => {
270                            println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
271                            return;
272                        }
273                        IdentifyUserResponse::IdentityVerificationAvailable => {
274                            state = IdentifyUserState::IdDisplayAndSubmit;
275                        }
276                        _ => {
277                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
278                            return;
279                        }
280                    }
281                }
282                IdentifyUserState::IdDisplayAndSubmit => {
283                    println!("When asked for your ID, provide the following: {self_id}");
284
285                    // Display Prompt
286                    let other_user_id: String = Input::new()
287                        .with_prompt("Ask for the other person's ID, and insert it here")
288                        .interact_text()
289                        .expect("Failed to interact with interactive session");
290                    let _ = stdout().flush();
291
292                    let res = match &client
293                        .idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
294                        .await
295                    {
296                        Ok(res) => res.clone(),
297                        Err(e) => {
298                            return server_error(e);
299                        }
300                    };
301                    match res {
302                        IdentifyUserResponse::WaitForCode => {
303                            state = IdentifyUserState::SubmitCode;
304
305                            other_id = Some(other_user_id);
306                        }
307                        IdentifyUserResponse::ProvideCode { step, totp } => {
308                            state = IdentifyUserState::DisplayCodeFirst {
309                                self_totp: totp,
310                                step,
311                            };
312
313                            other_id = Some(other_user_id);
314                        }
315                        _ => {
316                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
317                            return;
318                        }
319                    }
320                }
321                IdentifyUserState::SubmitCode => {
322                    // Display Prompt
323                    let other_totp: String = Input::new()
324                        .with_prompt("Insert here the other person code")
325                        .validate_with(|s: &String| -> Result<(), &str> {
326                            if VALIDATE_TOTP_RE.is_match(s) {
327                                Ok(())
328                            } else {
329                                Err("The code should be a 5 or 6 digit number")
330                            }
331                        })
332                        .interact_text()
333                        .expect("Failed to interact with interactive session");
334
335                    let res = match &client
336                        .idm_person_identify_user(
337                            other_id.as_deref().unwrap_or_default(),
338                            IdentifyUserRequest::SubmitCode {
339                                other_totp: other_totp.parse().unwrap_or_default(),
340                            },
341                        )
342                        .await
343                    {
344                        Ok(res) => res.clone(),
345                        Err(e) => {
346                            return server_error(e);
347                        }
348                    };
349                    match res {
350                        IdentifyUserResponse::CodeFailure => {
351                            eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
352                            return;
353                        }
354                        IdentifyUserResponse::Success => {
355                            println!(
356                                "{}'s identity has been successfully verified 🎉🎉",
357                                other_id.as_deref().unwrap_or_default()
358                            );
359                            return;
360                        }
361                        IdentifyUserResponse::ProvideCode { step, totp } => {
362                            // since we have already inserted the code, we have to go to display code second,
363                            state = IdentifyUserState::DisplayCodeSecond {
364                                self_totp: totp,
365                                step,
366                            };
367                        }
368
369                        _ => {
370                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
371                            return;
372                        }
373                    }
374                }
375                IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
376                    println!("Provide the following code when asked: {self_totp}");
377                    let seconds_left = get_ms_left_from_now(step as u128) / 1000;
378                    println!("This codes expires in {seconds_left} seconds");
379                    let _ = stdout().flush();
380                    if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
381                        println!("Identity verification failed. Exiting...");
382                        return;
383                    }
384                    match Confirm::new()
385                    .with_prompt(format!("Did you confirm that {} correctly verified your code? If you proceed, you won't be able to go back.", other_id.as_deref().unwrap_or_default()))
386                    .interact() {
387                        Ok(true) => {println!("Code confirmed, continuing...")}
388                        Ok(false) => {
389                            println!("Identity verification failed. Exiting...");
390                            return;
391                        },
392                        Err(e) => {
393                            eprintln!("An error occurred while trying to read from stderr: {e:?}"); // TODO: add error ID (internal error, restart)
394                            println!("Exiting...");
395                            return;
396                        },
397                        };
398
399                    state = IdentifyUserState::SubmitCode;
400                }
401                IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
402                    println!("Provide the following code when asked: {self_totp}");
403                    let seconds_left = get_ms_left_from_now(step as u128) / 1000;
404                    println!("This codes expires in {seconds_left} seconds!");
405                    let _ = stdout().flush();
406                    if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
407                        println!("Identity verification failed. Exiting...");
408                        return;
409                    }
410                    match Confirm::new()
411                    .with_prompt(format!("Did you confirm that {} correctly verified your code? If you proceed, you won't be able to go back.", other_id.as_deref().unwrap_or_default()))
412                    .interact() {
413                        Ok(true) => {println!(
414                            "{}'s identity has been successfully verified 🎉🎉",
415                            other_id.take().unwrap_or_default()
416                        );
417                        return;}
418                        Ok(false) => {
419                            println!("Exiting...");
420                            return;
421                        },
422                        Err(e) => {
423                            eprintln!("An error occurred while trying to read from stderr: {e:?}"); // TODO: add error ID (internal error, restart)
424                            println!("Exiting...");
425                            return;
426                        },
427                        };
428                }
429            }
430        }
431    }
432
433    fn get_ms_left_from_now(step: u128) -> u32 {
434        #[allow(clippy::expect_used)]
435        let dur = SystemTime::now()
436            .duration_since(SystemTime::UNIX_EPOCH)
437            .expect("invalid duration from epoch now");
438        let ms: u128 = dur.as_millis();
439        (step * 1000 - ms % (step * 1000)) as u32
440    }
441}