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::Synch { commands } => commands.exec(opt).await,
152        }
153    }
154}
155
156impl KanidmClientParser {
157    pub async fn exec(self) {
158        match self.commands.clone() {
159            KanidmClientOpt::Raw { commands } => commands.exec(self).await,
160            KanidmClientOpt::Login(lopt) => lopt.exec(self).await,
161            KanidmClientOpt::Reauth { mode } => self.reauth(mode).await,
162            KanidmClientOpt::Logout(lopt) => lopt.exec(self).await,
163            KanidmClientOpt::Session { commands } => commands.exec(self).await,
164            KanidmClientOpt::CSelf { commands } => commands.exec(self).await,
165            KanidmClientOpt::Person { commands } => commands.exec(self).await,
166            KanidmClientOpt::ServiceAccount { commands } => commands.exec(self).await,
167            KanidmClientOpt::Group { commands } => commands.exec(self).await,
168            KanidmClientOpt::Graph(gops) => gops.exec(self).await,
169            KanidmClientOpt::System { commands } => commands.exec(self).await,
170            KanidmClientOpt::Schema {
171                commands: SchemaOpt::Class { commands },
172            } => commands.exec(self).await,
173            KanidmClientOpt::Schema {
174                commands: SchemaOpt::Attribute { commands },
175            } => commands.exec(self).await,
176            KanidmClientOpt::Recycle { commands } => commands.exec(self).await,
177            KanidmClientOpt::Version => {
178                self.output_mode
179                    .print_message(format!("kanidm {}", env!("KANIDM_PKG_VERSION")));
180            }
181        }
182    }
183}
184
185pub(crate) fn password_prompt(prompt: &str) -> Option<String> {
186    for _ in 0..3 {
187        let password = dialoguer::Password::new()
188            .with_prompt(prompt)
189            .interact()
190            .ok()?;
191
192        let password_confirm = dialoguer::Password::new()
193            .with_prompt("Reenter the password to confirm: ")
194            .interact()
195            .ok()?;
196
197        if password == password_confirm {
198            return Some(password);
199        } else {
200            error!("Passwords do not match");
201        }
202    }
203    None
204}
205
206pub const IDENTITY_UNAVAILABLE_ERROR_MESSAGE: &str = "Identity verification is not available.";
207pub 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!";
208pub const INVALID_STATE_ERROR_MESSAGE: &str =
209    "The user identification flow is in an invalid state 😵😵";
210
211mod identify_user_no_tui {
212    use crate::{
213        CODE_FAILURE_ERROR_MESSAGE, IDENTITY_UNAVAILABLE_ERROR_MESSAGE, INVALID_STATE_ERROR_MESSAGE,
214    };
215
216    use kanidm_client::{ClientError, KanidmClient};
217    use kanidm_proto::internal::{IdentifyUserRequest, IdentifyUserResponse};
218
219    use dialoguer::{Confirm, Input};
220    use regex::Regex;
221    use std::{
222        io::{stdout, Write},
223        time::SystemTime,
224    };
225
226    lazy_static::lazy_static! {
227        pub static ref VALIDATE_TOTP_RE: Regex = {
228            #[allow(clippy::expect_used)]
229            Regex::new(r"^\d{5,6}$").expect("Failed to parse VALIDATE_TOTP_RE") // TODO: add an error ID (internal error, restart)
230        };
231    }
232
233    pub(super) enum IdentifyUserState {
234        Start,
235        IdDisplayAndSubmit,
236        SubmitCode,
237        DisplayCodeFirst { self_totp: u32, step: u32 },
238        DisplayCodeSecond { self_totp: u32, step: u32 },
239    }
240
241    fn server_error(e: &ClientError) {
242        eprintln!("Server error!"); // TODO: add an error ID (internal error, restart)
243        eprintln!("{e:?}");
244        println!("Exiting...");
245    }
246
247    pub(super) async fn run_identity_verification_no_tui(
248        mut state: IdentifyUserState,
249        client: KanidmClient,
250        self_id: &str,
251        mut other_id: Option<String>,
252    ) {
253        loop {
254            match state {
255                IdentifyUserState::Start => {
256                    let res = match &client
257                        .idm_person_identify_user(self_id, IdentifyUserRequest::Start)
258                        .await
259                    {
260                        Ok(res) => res.clone(),
261                        Err(e) => {
262                            return server_error(e);
263                        }
264                    };
265                    match res {
266                        IdentifyUserResponse::IdentityVerificationUnavailable => {
267                            println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
268                            return;
269                        }
270                        IdentifyUserResponse::IdentityVerificationAvailable => {
271                            state = IdentifyUserState::IdDisplayAndSubmit;
272                        }
273                        _ => {
274                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
275                            return;
276                        }
277                    }
278                }
279                IdentifyUserState::IdDisplayAndSubmit => {
280                    println!("When asked for your ID, provide the following: {self_id}");
281
282                    // Display Prompt
283                    let other_user_id: String = Input::new()
284                        .with_prompt("Ask for the other person's ID, and insert it here")
285                        .interact_text()
286                        .expect("Failed to interact with interactive session");
287                    let _ = stdout().flush();
288
289                    let res = match &client
290                        .idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
291                        .await
292                    {
293                        Ok(res) => res.clone(),
294                        Err(e) => {
295                            return server_error(e);
296                        }
297                    };
298                    match res {
299                        IdentifyUserResponse::WaitForCode => {
300                            state = IdentifyUserState::SubmitCode;
301
302                            other_id = Some(other_user_id);
303                        }
304                        IdentifyUserResponse::ProvideCode { step, totp } => {
305                            state = IdentifyUserState::DisplayCodeFirst {
306                                self_totp: totp,
307                                step,
308                            };
309
310                            other_id = Some(other_user_id);
311                        }
312                        _ => {
313                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
314                            return;
315                        }
316                    }
317                }
318                IdentifyUserState::SubmitCode => {
319                    // Display Prompt
320                    let other_totp: String = Input::new()
321                        .with_prompt("Insert here the other person code")
322                        .validate_with(|s: &String| -> Result<(), &str> {
323                            if VALIDATE_TOTP_RE.is_match(s) {
324                                Ok(())
325                            } else {
326                                Err("The code should be a 5 or 6 digit number")
327                            }
328                        })
329                        .interact_text()
330                        .expect("Failed to interact with interactive session");
331
332                    let res = match &client
333                        .idm_person_identify_user(
334                            other_id.as_deref().unwrap_or_default(),
335                            IdentifyUserRequest::SubmitCode {
336                                other_totp: other_totp.parse().unwrap_or_default(),
337                            },
338                        )
339                        .await
340                    {
341                        Ok(res) => res.clone(),
342                        Err(e) => {
343                            return server_error(e);
344                        }
345                    };
346                    match res {
347                        IdentifyUserResponse::CodeFailure => {
348                            eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
349                            return;
350                        }
351                        IdentifyUserResponse::Success => {
352                            println!(
353                                "{}'s identity has been successfully verified 🎉🎉",
354                                other_id.as_deref().unwrap_or_default()
355                            );
356                            return;
357                        }
358                        IdentifyUserResponse::ProvideCode { step, totp } => {
359                            // since we have already inserted the code, we have to go to display code second,
360                            state = IdentifyUserState::DisplayCodeSecond {
361                                self_totp: totp,
362                                step,
363                            };
364                        }
365
366                        _ => {
367                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
368                            return;
369                        }
370                    }
371                }
372                IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
373                    println!("Provide the following code when asked: {self_totp}");
374                    let seconds_left = get_ms_left_from_now(step as u128) / 1000;
375                    println!("This codes expires in {seconds_left} seconds");
376                    let _ = stdout().flush();
377                    if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
378                        println!("Identity verification failed. Exiting...");
379                        return;
380                    }
381                    match Confirm::new()
382                    .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()))
383                    .interact() {
384                        Ok(true) => {println!("Code confirmed, continuing...")}
385                        Ok(false) => {
386                            println!("Identity verification failed. Exiting...");
387                            return;
388                        },
389                        Err(e) => {
390                            eprintln!("An error occurred while trying to read from stderr: {e:?}"); // TODO: add error ID (internal error, restart)
391                            println!("Exiting...");
392                            return;
393                        },
394                        };
395
396                    state = IdentifyUserState::SubmitCode;
397                }
398                IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
399                    println!("Provide the following code when asked: {self_totp}");
400                    let seconds_left = get_ms_left_from_now(step as u128) / 1000;
401                    println!("This codes expires in {seconds_left} seconds!");
402                    let _ = stdout().flush();
403                    if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
404                        println!("Identity verification failed. Exiting...");
405                        return;
406                    }
407                    match Confirm::new()
408                    .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()))
409                    .interact() {
410                        Ok(true) => {println!(
411                            "{}'s identity has been successfully verified 🎉🎉",
412                            other_id.take().unwrap_or_default()
413                        );
414                        return;}
415                        Ok(false) => {
416                            println!("Exiting...");
417                            return;
418                        },
419                        Err(e) => {
420                            eprintln!("An error occurred while trying to read from stderr: {e:?}"); // TODO: add error ID (internal error, restart)
421                            println!("Exiting...");
422                            return;
423                        },
424                        };
425                }
426            }
427        }
428    }
429
430    fn get_ms_left_from_now(step: u128) -> u32 {
431        #[allow(clippy::expect_used)]
432        let dur = SystemTime::now()
433            .duration_since(SystemTime::UNIX_EPOCH)
434            .expect("invalid duration from epoch now");
435        let ms: u128 = dur.as_millis();
436        (step * 1000 - ms % (step * 1000)) as u32
437    }
438}