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