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