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                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 { mode } => self.reauth(mode).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        time::SystemTime,
228    };
229
230    lazy_static::lazy_static! {
231        pub static ref VALIDATE_TOTP_RE: Regex = {
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
237    pub(super) enum IdentifyUserState {
238        Start,
239        IdDisplayAndSubmit,
240        SubmitCode,
241        DisplayCodeFirst { self_totp: u32, step: u32 },
242        DisplayCodeSecond { self_totp: u32, step: u32 },
243    }
244
245    fn server_error(e: &ClientError) {
246        eprintln!("Server error!"); // TODO: add an error ID (internal error, restart)
247        eprintln!("{e:?}");
248        println!("Exiting...");
249    }
250
251    pub(super) async fn run_identity_verification_no_tui(
252        mut state: IdentifyUserState,
253        client: KanidmClient,
254        self_id: &str,
255        mut other_id: Option<String>,
256    ) {
257        loop {
258            match state {
259                IdentifyUserState::Start => {
260                    let res = match &client
261                        .idm_person_identify_user(self_id, IdentifyUserRequest::Start)
262                        .await
263                    {
264                        Ok(res) => res.clone(),
265                        Err(e) => {
266                            return server_error(e);
267                        }
268                    };
269                    match res {
270                        IdentifyUserResponse::IdentityVerificationUnavailable => {
271                            println!("{IDENTITY_UNAVAILABLE_ERROR_MESSAGE}");
272                            return;
273                        }
274                        IdentifyUserResponse::IdentityVerificationAvailable => {
275                            state = IdentifyUserState::IdDisplayAndSubmit;
276                        }
277                        _ => {
278                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
279                            return;
280                        }
281                    }
282                }
283                IdentifyUserState::IdDisplayAndSubmit => {
284                    println!("When asked for your ID, provide the following: {self_id}");
285
286                    // Display Prompt
287                    let other_user_id: String = Input::new()
288                        .with_prompt("Ask for the other person's ID, and insert it here")
289                        .interact_text()
290                        .expect("Failed to interact with interactive session");
291                    let _ = stdout().flush();
292
293                    let res = match &client
294                        .idm_person_identify_user(&other_user_id, IdentifyUserRequest::Start)
295                        .await
296                    {
297                        Ok(res) => res.clone(),
298                        Err(e) => {
299                            return server_error(e);
300                        }
301                    };
302                    match res {
303                        IdentifyUserResponse::WaitForCode => {
304                            state = IdentifyUserState::SubmitCode;
305
306                            other_id = Some(other_user_id);
307                        }
308                        IdentifyUserResponse::ProvideCode { step, totp } => {
309                            state = IdentifyUserState::DisplayCodeFirst {
310                                self_totp: totp,
311                                step,
312                            };
313
314                            other_id = Some(other_user_id);
315                        }
316                        _ => {
317                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
318                            return;
319                        }
320                    }
321                }
322                IdentifyUserState::SubmitCode => {
323                    // Display Prompt
324                    let other_totp: String = Input::new()
325                        .with_prompt("Insert here the other person code")
326                        .validate_with(|s: &String| -> Result<(), &str> {
327                            if VALIDATE_TOTP_RE.is_match(s) {
328                                Ok(())
329                            } else {
330                                Err("The code should be a 5 or 6 digit number")
331                            }
332                        })
333                        .interact_text()
334                        .expect("Failed to interact with interactive session");
335
336                    let res = match &client
337                        .idm_person_identify_user(
338                            other_id.as_deref().unwrap_or_default(),
339                            IdentifyUserRequest::SubmitCode {
340                                other_totp: other_totp.parse().unwrap_or_default(),
341                            },
342                        )
343                        .await
344                    {
345                        Ok(res) => res.clone(),
346                        Err(e) => {
347                            return server_error(e);
348                        }
349                    };
350                    match res {
351                        IdentifyUserResponse::CodeFailure => {
352                            eprintln!("{CODE_FAILURE_ERROR_MESSAGE}");
353                            return;
354                        }
355                        IdentifyUserResponse::Success => {
356                            println!(
357                                "{}'s identity has been successfully verified 🎉🎉",
358                                other_id.as_deref().unwrap_or_default()
359                            );
360                            return;
361                        }
362                        IdentifyUserResponse::ProvideCode { step, totp } => {
363                            // since we have already inserted the code, we have to go to display code second,
364                            state = IdentifyUserState::DisplayCodeSecond {
365                                self_totp: totp,
366                                step,
367                            };
368                        }
369
370                        _ => {
371                            eprintln!("{INVALID_STATE_ERROR_MESSAGE}");
372                            return;
373                        }
374                    }
375                }
376                IdentifyUserState::DisplayCodeFirst { self_totp, step } => {
377                    println!("Provide the following code when asked: {self_totp}");
378                    let seconds_left = get_ms_left_from_now(step as u128) / 1000;
379                    println!("This codes expires in {seconds_left} seconds");
380                    let _ = stdout().flush();
381                    if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
382                        println!("Identity verification failed. Exiting...");
383                        return;
384                    }
385                    match Confirm::new()
386                    .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()))
387                    .interact() {
388                        Ok(true) => {println!("Code confirmed, continuing...")}
389                        Ok(false) => {
390                            println!("Identity verification failed. Exiting...");
391                            return;
392                        },
393                        Err(e) => {
394                            eprintln!("An error occurred while trying to read from stderr: {e:?}"); // TODO: add error ID (internal error, restart)
395                            println!("Exiting...");
396                            return;
397                        },
398                        };
399
400                    state = IdentifyUserState::SubmitCode;
401                }
402                IdentifyUserState::DisplayCodeSecond { self_totp, step } => {
403                    println!("Provide the following code when asked: {self_totp}");
404                    let seconds_left = get_ms_left_from_now(step as u128) / 1000;
405                    println!("This codes expires in {seconds_left} seconds!");
406                    let _ = stdout().flush();
407                    if !matches!(Confirm::new().with_prompt("Continue?").interact(), Ok(true)) {
408                        println!("Identity verification failed. Exiting...");
409                        return;
410                    }
411                    match Confirm::new()
412                    .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()))
413                    .interact() {
414                        Ok(true) => {println!(
415                            "{}'s identity has been successfully verified 🎉🎉",
416                            other_id.take().unwrap_or_default()
417                        );
418                        return;}
419                        Ok(false) => {
420                            println!("Exiting...");
421                            return;
422                        },
423                        Err(e) => {
424                            eprintln!("An error occurred while trying to read from stderr: {e:?}"); // TODO: add error ID (internal error, restart)
425                            println!("Exiting...");
426                            return;
427                        },
428                        };
429                }
430            }
431        }
432    }
433
434    fn get_ms_left_from_now(step: u128) -> u32 {
435        #[allow(clippy::expect_used)]
436        let dur = SystemTime::now()
437            .duration_since(SystemTime::UNIX_EPOCH)
438            .expect("invalid duration from epoch now");
439        let ms: u128 = dur.as_millis();
440        (step * 1000 - ms % (step * 1000)) as u32
441    }
442}