kanidm_cli/
common.rs

1use std::env;
2
3use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError};
4use dialoguer::theme::ColorfulTheme;
5use dialoguer::{Confirm, Select};
6use kanidm_client::{KanidmClient, KanidmClientBuilder};
7use kanidm_proto::constants::{DEFAULT_CLIENT_CONFIG_PATH, DEFAULT_CLIENT_CONFIG_PATH_HOME};
8use kanidm_proto::internal::UserAuthToken;
9use time::format_description::well_known::Rfc3339;
10use time::OffsetDateTime;
11
12use crate::session::read_tokens;
13use crate::{CommonOpt, LoginOpt, ReauthOpt};
14
15#[derive(Clone)]
16pub enum OpType {
17    Read,
18    Write,
19}
20
21#[derive(Debug)]
22#[allow(clippy::large_enum_variant)]
23pub enum ToClientError {
24    NeedLogin(String),
25    NeedReauth(String, KanidmClient),
26    Other,
27}
28
29impl CommonOpt {
30    pub fn to_unauth_client(&self) -> KanidmClient {
31        let config_path: String = shellexpand::tilde(DEFAULT_CLIENT_CONFIG_PATH_HOME).into_owned();
32
33        let instance_name: Option<&str> = self.instance.as_deref();
34
35        let client_builder = KanidmClientBuilder::new()
36            .read_options_from_optional_instance_config(DEFAULT_CLIENT_CONFIG_PATH, instance_name)
37            .map_err(|e| {
38                error!(
39                    "Failed to parse config ({:?}) -- {:?}",
40                    DEFAULT_CLIENT_CONFIG_PATH, e
41                );
42                e
43            })
44            .and_then(|cb| {
45                cb.read_options_from_optional_instance_config(&config_path, instance_name)
46                    .map_err(|e| {
47                        error!("Failed to parse config ({:?}) -- {:?}", config_path, e);
48                        e
49                    })
50            })
51            .unwrap_or_else(|_e| {
52                std::process::exit(1);
53            });
54        debug!(
55            "Successfully loaded configuration, looked in {} and {} - client builder state: {:?}",
56            DEFAULT_CLIENT_CONFIG_PATH, DEFAULT_CLIENT_CONFIG_PATH_HOME, &client_builder
57        );
58
59        let client_builder = match &self.addr {
60            Some(a) => client_builder.address(a.to_string()),
61            None => client_builder,
62        };
63
64        let ca_path: Option<&str> = self.ca_path.as_ref().and_then(|p| p.to_str());
65        let client_builder = match ca_path {
66            Some(p) => {
67                debug!("Adding trusted CA cert {:?}", p);
68                let client_builder = client_builder
69                    .add_root_certificate_filepath(p)
70                    .unwrap_or_else(|e| {
71                        error!("Failed to add ca certificate -- {:?}", e);
72                        std::process::exit(1);
73                    });
74
75                debug!(
76                    "After attempting to add trusted CA cert, client builder state: {:?}",
77                    client_builder
78                );
79                client_builder
80            }
81            None => client_builder,
82        };
83
84        let client_builder = match self.skip_hostname_verification {
85            true => {
86                warn!(
87                    "Accepting invalid hostnames on the certificate for {:?}",
88                    &self.addr
89                );
90                client_builder.danger_accept_invalid_hostnames(true)
91            }
92            false => client_builder,
93        };
94
95        let client_builder = match self.accept_invalid_certs {
96            true => {
97                warn!(
98                    "TLS Certificate Verification disabled!!! This can lead to credential and account compromise!!!"
99                );
100                client_builder.danger_accept_invalid_certs(true)
101            }
102            false => client_builder,
103        };
104
105        let client_builder = client_builder.set_token_cache_path(self.token_cache_path.clone());
106
107        client_builder.build().unwrap_or_else(|e| {
108            error!("Failed to build client instance -- {:?}", e);
109            std::process::exit(1);
110        })
111    }
112
113    pub(crate) async fn try_to_client(
114        &self,
115        optype: OpType,
116    ) -> Result<KanidmClient, ToClientError> {
117        let client = self.to_unauth_client();
118
119        // Read the token file.
120        let token_store = match read_tokens(&client.get_token_cache_path()) {
121            Ok(t) => t,
122            Err(_e) => {
123                error!("Error retrieving authentication token store");
124                return Err(ToClientError::Other);
125            }
126        };
127
128        let Some(token_instance) = token_store.instances(&self.instance) else {
129            error!(
130                "No valid authentication tokens found. Please login with the 'login' subcommand."
131            );
132            return Err(ToClientError::Other);
133        };
134
135        // If we have a username, use that to select tokens
136        let (spn, jwsc) = match &self.username {
137            Some(filter_username) => {
138                let possible_token = if filter_username.contains('@') {
139                    // If there is an @, it's an spn so just get the token directly.
140                    token_instance
141                        .tokens()
142                        .get(filter_username)
143                        .map(|t| (filter_username.clone(), t.clone()))
144                } else {
145                    // first we try to find user@hostname
146                    let filter_username_with_hostname = format!(
147                        "{}@{}",
148                        filter_username,
149                        client.get_origin().host_str().unwrap_or("localhost")
150                    );
151                    debug!(
152                        "Looking for tokens matching {}",
153                        filter_username_with_hostname
154                    );
155
156                    let mut token_refs: Vec<_> = token_instance
157                        .tokens()
158                        .iter()
159                        .filter(|(t, _)| *t == &filter_username_with_hostname)
160                        .map(|(k, v)| (k.clone(), v.clone()))
161                        .collect();
162
163                    if token_refs.len() == 1 {
164                        // return the token
165                        token_refs.pop()
166                    } else {
167                        // otherwise let's try the fallback
168                        let filter_username = format!("{}@", filter_username);
169                        // Filter for tokens that match the pattern
170                        let mut token_refs: Vec<_> = token_instance
171                            .tokens()
172                            .iter()
173                            .filter(|(t, _)| t.starts_with(&filter_username))
174                            .map(|(s, j)| (s.clone(), j.clone()))
175                            .collect();
176
177                        match token_refs.len() {
178                            0 => None,
179                            1 => token_refs.pop(),
180                            _ => {
181                                error!("Multiple authentication tokens found for {}. Please specify the full spn to proceed", filter_username);
182                                return Err(ToClientError::Other);
183                            }
184                        }
185                    }
186                };
187
188                // Is it in the store?
189                match possible_token {
190                    Some(t) => t,
191                    None => {
192                        error!(
193                            "No valid authentication tokens found for {}.",
194                            filter_username
195                        );
196                        return Err(ToClientError::NeedLogin(filter_username.clone()));
197                    }
198                }
199            }
200            None => {
201                if token_instance.tokens().len() == 1 {
202                    #[allow(clippy::expect_used)]
203                    let (f_uname, f_token) = token_instance
204                        .tokens()
205                        .iter()
206                        .next()
207                        .expect("Memory Corruption");
208                    // else pick the first token
209                    debug!("Using cached token for name {}", f_uname);
210                    (f_uname.clone(), f_token.clone())
211                } else {
212                    // Unable to automatically select the user because multiple tokens exist
213                    // so we'll prompt the user to select one
214                    match prompt_for_username_get_values(
215                        &client.get_token_cache_path(),
216                        &self.instance,
217                    ) {
218                        Ok(tuple) => tuple,
219                        Err(msg) => {
220                            error!("Error: {}", msg);
221                            std::process::exit(1);
222                        }
223                    }
224                }
225            }
226        };
227
228        let Some(key_id) = jwsc.kid() else {
229            error!("token invalid, not key id associated");
230            return Err(ToClientError::Other);
231        };
232
233        let Some(pub_jwk) = token_instance.keys().get(key_id) else {
234            error!("token invalid, no cached jwk available");
235            return Err(ToClientError::Other);
236        };
237
238        // Is the token (probably) valid?
239        let jws_verifier = match JwsEs256Verifier::try_from(pub_jwk) {
240            Ok(verifier) => verifier,
241            Err(err) => {
242                error!(?err, "Unable to configure jws verifier");
243                return Err(ToClientError::Other);
244            }
245        };
246
247        match jws_verifier.verify(&jwsc).and_then(|jws| {
248            jws.from_json::<UserAuthToken>().map_err(|serde_err| {
249                error!(?serde_err);
250                JwtError::InvalidJwt
251            })
252        }) {
253            Ok(uat) => {
254                let now_utc = time::OffsetDateTime::now_utc();
255                if let Some(exp) = uat.expiry {
256                    if now_utc >= exp {
257                        error!(
258                            "Session has expired for {} - you may need to login again.",
259                            uat.spn
260                        );
261                        return Err(ToClientError::NeedLogin(spn));
262                    }
263                }
264
265                // It's probably valid, set into the client
266                client.set_token(jwsc.to_string()).await;
267
268                // Check what we are doing based on op.
269                match optype {
270                    OpType::Read => {}
271                    OpType::Write => {
272                        if !uat.purpose_readwrite_active(now_utc + time::Duration::new(20, 0)) {
273                            error!(
274                                "Privileges have expired for {} - you need to re-authenticate again.",
275                                uat.spn
276                            );
277                            return Err(ToClientError::NeedReauth(spn, client));
278                        }
279                    }
280                }
281            }
282            Err(e) => {
283                error!("Unable to read token for requested user - you may need to login again.");
284                debug!(?e, "JWT Error");
285                return Err(ToClientError::NeedLogin(spn));
286            }
287        };
288
289        Ok(client)
290    }
291
292    pub async fn to_client(&self, optype: OpType) -> KanidmClient {
293        let mut copt_mut = self.clone();
294        loop {
295            match self.try_to_client(optype.clone()).await {
296                Ok(c) => break c,
297                Err(ToClientError::NeedLogin(username)) => {
298                    if !Confirm::new()
299                        .with_prompt("Would you like to login again?")
300                        .default(true)
301                        .interact()
302                        .expect("Failed to interact with interactive session")
303                    {
304                        std::process::exit(1);
305                    }
306
307                    copt_mut.username = Some(username);
308                    let copt = copt_mut.clone();
309                    let login_opt = LoginOpt {
310                        copt,
311                        password: env::var("KANIDM_PASSWORD").ok(),
312                    };
313
314                    login_opt.exec().await;
315                    // Okay, try again ...
316                    continue;
317                }
318                Err(ToClientError::NeedReauth(username, client)) => {
319                    if !Confirm::new()
320                        .with_prompt("Would you like to re-authenticate?")
321                        .default(true)
322                        .interact()
323                        .expect("Failed to interact with interactive session")
324                    {
325                        std::process::exit(1);
326                    }
327                    copt_mut.username = Some(username);
328                    let copt = copt_mut.clone();
329                    let reauth_opt = ReauthOpt { copt };
330                    reauth_opt.inner(client).await;
331
332                    // Okay, re-auth should have passed, lets loop
333                    continue;
334                }
335                Err(ToClientError::Other) => {
336                    std::process::exit(1);
337                }
338            }
339        }
340    }
341}
342
343/// This parses the token store and prompts the user to select their username, returns the username/token as a tuple of Strings
344///
345/// Used to reduce duplication in implementing [prompt_for_username_get_username] and `prompt_for_username_get_token`
346pub fn prompt_for_username_get_values(
347    token_cache_path: &str,
348    instance_name: &Option<String>,
349) -> Result<(String, JwsCompact), String> {
350    let token_store = match read_tokens(token_cache_path) {
351        Ok(value) => value,
352        _ => return Err("Error retrieving authentication token store".to_string()),
353    };
354
355    let Some(token_instance) = token_store.instances(instance_name) else {
356        error!("No tokens in store, quitting!");
357        std::process::exit(1);
358    };
359
360    if token_instance.tokens().is_empty() {
361        error!("No tokens in store, quitting!");
362        std::process::exit(1);
363    }
364    let mut options = Vec::new();
365    for option in token_instance.tokens().iter() {
366        options.push(String::from(option.0));
367    }
368    let user_select = Select::with_theme(&ColorfulTheme::default())
369        .with_prompt("Multiple authentication tokens exist. Please select one")
370        .default(0)
371        .items(&options)
372        .interact();
373    let selection = match user_select {
374        Err(error) => {
375            error!("Failed to handle user input: {:?}", error);
376            std::process::exit(1);
377        }
378        Ok(value) => value,
379    };
380    debug!("Index of the chosen menu item: {:?}", selection);
381
382    match token_instance.tokens().iter().nth(selection) {
383        Some(value) => {
384            let (f_uname, f_token) = value;
385            debug!("Using cached token for name {}", f_uname);
386            debug!("Cached token: {}", f_token);
387            Ok((f_uname.to_string(), f_token.clone()))
388        }
389        None => {
390            error!("Memory corruption trying to read token store, quitting!");
391            std::process::exit(1);
392        }
393    }
394}
395
396/// This parses the token store and prompts the user to select their username, returns the username as a String
397///
398/// Powered by [prompt_for_username_get_values]
399pub fn prompt_for_username_get_username(
400    token_cache_path: &str,
401    instance_name: &Option<String>,
402) -> Result<String, String> {
403    match prompt_for_username_get_values(token_cache_path, instance_name) {
404        Ok(value) => {
405            let (f_user, _) = value;
406            Ok(f_user)
407        }
408        Err(err) => Err(err),
409    }
410}
411
412/*
413/// This parses the token store and prompts the user to select their username, returns the token as a String
414///
415/// Powered by [prompt_for_username_get_values]
416pub fn prompt_for_username_get_token() -> Result<String, String> {
417    match prompt_for_username_get_values() {
418        Ok(value) => {
419            let (_, f_token) = value;
420            Ok(f_token)
421        }
422        Err(err) => Err(err),
423    }
424}
425*/
426
427/// This parses the input for the person/service-account expire-at CLI commands
428///
429/// If it fails, return error, if it needs to *clear* the result, return Ok(None),
430/// otherwise return Ok(Some(String)) which is the new value to set.
431pub(crate) fn try_expire_at_from_string(input: &str) -> Result<Option<String>, ()> {
432    match input {
433        "any" | "never" | "clear" => Ok(None),
434        "now" => match OffsetDateTime::now_utc().format(&Rfc3339) {
435            Ok(s) => Ok(Some(s)),
436            Err(e) => {
437                error!(err = ?e, "Unable to format current time to rfc3339");
438                Err(())
439            }
440        },
441        "epoch" => match OffsetDateTime::UNIX_EPOCH.format(&Rfc3339) {
442            Ok(val) => Ok(Some(val)),
443            Err(err) => {
444                error!("Failed to format epoch timestamp as RFC3339: {:?}", err);
445                Err(())
446            }
447        },
448        _ => {
449            // fall back to parsing it as a date
450            match OffsetDateTime::parse(input, &Rfc3339) {
451                Ok(_) => Ok(Some(input.to_string())),
452                Err(err) => {
453                    error!("Failed to parse supplied timestamp: {:?}", err);
454                    Err(())
455                }
456            }
457        }
458    }
459}