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