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