kanidm_cli/
session.rs

1use crate::common::{OpType, ToClientError};
2use std::cmp::Reverse;
3use std::collections::BTreeMap;
4use std::fs::{create_dir, File};
5use std::io::{self, BufReader, BufWriter, ErrorKind, IsTerminal, Write};
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use compact_jwt::{
10    traits::JwsVerifiable, Jwk, JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError,
11};
12use dialoguer::theme::ColorfulTheme;
13use dialoguer::Select;
14use kanidm_client::{ClientError, KanidmClient};
15use kanidm_proto::constants::CLIENT_TOKEN_CACHE;
16use kanidm_proto::internal::UserAuthToken;
17use kanidm_proto::v1::{AuthAllowed, AuthResponse, AuthState};
18#[cfg(target_family = "unix")]
19use libc::umask;
20use webauthn_authenticator_rs::prelude::RequestChallengeResponse;
21
22use crate::common::prompt_for_username_get_username;
23use crate::webauthn::get_authenticator;
24use crate::{CommonOpt, LoginOpt, LogoutOpt, ReauthOpt, SessionOpt};
25
26use serde::{Deserialize, Serialize};
27
28static TOKEN_DIR: &str = "~/.cache";
29
30#[derive(Debug, Serialize, Clone, Deserialize, Default)]
31pub struct TokenInstance {
32    keys: BTreeMap<String, Jwk>,
33    tokens: BTreeMap<String, JwsCompact>,
34}
35
36impl TokenInstance {
37    pub fn tokens(&self) -> &BTreeMap<String, JwsCompact> {
38        &self.tokens
39    }
40
41    pub fn keys(&self) -> &BTreeMap<String, Jwk> {
42        &self.keys
43    }
44
45    pub fn valid_uats(&self) -> BTreeMap<String, UserAuthToken> {
46        self.tokens
47            .iter()
48            .filter_map(|(u, jwsc)| {
49                // Ignore if it has no key id.
50                let key_id = jwsc.kid()?;
51
52                // Ignore if we can't verify
53                let pub_jwk = self.keys.get(key_id)?;
54
55                let jws_verifier = JwsEs256Verifier::try_from(pub_jwk)
56                    .map_err(|e| {
57                        error!(?e, "Unable to configure jws verifier");
58                    })
59                    .ok()?;
60
61                jws_verifier
62                    .verify(jwsc)
63                    .and_then(|jws| {
64                        jws.from_json::<UserAuthToken>().map_err(|serde_err| {
65                            error!(?serde_err);
66                            JwtError::InvalidJwt
67                        })
68                    })
69                    .map_err(|e| {
70                        error!(?e, "Unable to verify token signature, may be corrupt");
71                    })
72                    .map(|uat| (u.clone(), uat))
73                    .ok()
74            })
75            .collect()
76    }
77
78    pub fn cleanup(&mut self, now: time::OffsetDateTime) -> usize {
79        // It's not optimal to do this in this way, but we can't double borrow.
80        let retain = self.valid_uats();
81
82        let start_len = self.tokens.len();
83
84        self.tokens.retain(|spn, _tonk| {
85            if let Some(uat) = retain.get(spn) {
86                if let Some(exp) = uat.expiry {
87                    // Retain if expiry is in future aka greater than now
88                    exp > now
89                } else {
90                    true
91                }
92            } else {
93                false
94            }
95        });
96
97        start_len - self.tokens.len()
98    }
99}
100
101#[derive(Debug, Serialize, Clone, Deserialize, Default)]
102pub struct TokenStore {
103    instances: BTreeMap<String, TokenInstance>,
104}
105
106impl TokenStore {
107    pub fn instances(&self, name: &Option<String>) -> Option<&TokenInstance> {
108        let n_lookup = name.clone().unwrap_or_default();
109
110        self.instances.get(&n_lookup)
111    }
112
113    pub fn instances_mut(&mut self, name: &Option<String>) -> Option<&mut TokenInstance> {
114        let n_lookup = name.clone().unwrap_or_default();
115
116        self.instances.get_mut(&n_lookup)
117    }
118}
119
120impl CommonOpt {
121    fn get_token_cache_path(&self) -> String {
122        match self.token_cache_path.clone() {
123            None => CLIENT_TOKEN_CACHE.to_string(),
124            Some(val) => val.clone(),
125        }
126    }
127}
128
129#[allow(clippy::result_unit_err)]
130pub fn read_tokens(token_path: &str) -> Result<TokenStore, ()> {
131    let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned());
132    if !token_path.exists() {
133        debug!(
134            "Token cache file path {:?} does not exist, returning an empty token store.",
135            token_path
136        );
137        return Ok(Default::default());
138    }
139
140    debug!("Attempting to read tokens from {:?}", &token_path);
141    // If the file does not exist, return Ok<map>
142    let file = match File::open(&token_path) {
143        Ok(f) => f,
144        Err(e) => {
145            match e.kind() {
146                ErrorKind::PermissionDenied => {
147                    // we bail here because you won't be able to write them back...
148                    error!(
149                        "Permission denied reading token store file {:?}",
150                        &token_path
151                    );
152                    return Err(());
153                }
154                // other errors are OK to continue past
155                _ => {
156                    warn!(
157                        "Cannot read tokens from {} due to error: {:?} ... continuing.",
158                        token_path.display(),
159                        e
160                    );
161                    return Ok(Default::default());
162                }
163            };
164        }
165    };
166    let reader = BufReader::new(file);
167
168    // Else try to read
169    serde_json::from_reader(reader).map_err(|e| {
170        warn!(
171            "JSON/IO error reading tokens from {:?} -> {:?}",
172            &token_path, e
173        );
174    })
175}
176
177#[allow(clippy::result_unit_err)]
178pub fn write_tokens(tokens: &TokenStore, token_path: &str) -> Result<(), ()> {
179    let token_dir = PathBuf::from(shellexpand::tilde(TOKEN_DIR).into_owned());
180    let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned());
181
182    token_dir
183        .parent()
184        .ok_or_else(|| {
185            error!(
186                "Parent directory to {} is invalid (root directory?).",
187                TOKEN_DIR
188            );
189        })
190        .and_then(|parent_dir| {
191            if parent_dir.exists() {
192                Ok(())
193            } else {
194                error!("Parent directory to {} does not exist.", TOKEN_DIR);
195                Err(())
196            }
197        })?;
198
199    if !token_dir.exists() {
200        create_dir(token_dir).map_err(|e| {
201            error!("Unable to create directory - {} {:?}", TOKEN_DIR, e);
202        })?;
203    }
204
205    // Take away group/everyone read/write
206    #[cfg(target_family = "unix")]
207    let before = unsafe { umask(0o177) };
208
209    let file = File::create(&token_path).map_err(|e| {
210        #[cfg(target_family = "unix")]
211        let _ = unsafe { umask(before) };
212        error!("Can not write to {} -> {:?}", token_path.display(), e);
213    })?;
214
215    #[cfg(target_family = "unix")]
216    let _ = unsafe { umask(before) };
217
218    let writer = BufWriter::new(file);
219    serde_json::to_writer_pretty(writer, tokens).map_err(|e| {
220        error!(
221            "JSON/IO error writing tokens to file {:?} -> {:?}",
222            &token_path, e
223        );
224    })
225}
226
227/// An interactive dialog to choose from given options
228fn get_index_choice_dialoguer(msg: &str, options: &[String]) -> usize {
229    let user_select = Select::with_theme(&ColorfulTheme::default())
230        .with_prompt(msg)
231        .default(0)
232        .items(options)
233        .interact();
234
235    let selection = match user_select {
236        Err(error) => {
237            error!("Failed to handle user input: {:?}", error);
238            std::process::exit(1);
239        }
240        Ok(value) => value,
241    };
242    debug!("Index of the chosen menu item: {:?}", selection);
243
244    selection
245}
246
247async fn do_password(
248    client: &mut KanidmClient,
249    password: &Option<String>,
250) -> Result<AuthResponse, ClientError> {
251    let password = match password {
252        Some(password) => {
253            trace!("User provided password directly, don't need to prompt.");
254            password.to_owned()
255        }
256        None => dialoguer::Password::new()
257            .with_prompt("Enter password")
258            .interact()
259            .unwrap_or_else(|e| {
260                error!("Failed to create password prompt -- {:?}", e);
261                std::process::exit(1);
262            }),
263    };
264    client.auth_step_password(password.as_str()).await
265}
266
267async fn do_backup_code(client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
268    print!("Enter Backup Code: ");
269    // We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
270    #[allow(clippy::unwrap_used)]
271    io::stdout().flush().unwrap();
272    let mut backup_code = String::new();
273    loop {
274        if let Err(e) = io::stdin().read_line(&mut backup_code) {
275            error!("Failed to read from stdin -> {:?}", e);
276            return Err(ClientError::SystemError);
277        };
278        if !backup_code.trim().is_empty() {
279            break;
280        };
281    }
282    client.auth_step_backup_code(backup_code.trim()).await
283}
284
285async fn do_totp(client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
286    let totp = loop {
287        print!("Enter TOTP: ");
288        // We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
289        if let Err(e) = io::stdout().flush() {
290            error!("Somehow we failed to flush stdout: {:?}", e);
291        };
292        let mut buffer = String::new();
293        if let Err(e) = io::stdin().read_line(&mut buffer) {
294            error!("Failed to read from stdin -> {:?}", e);
295            return Err(ClientError::SystemError);
296        };
297
298        let response = buffer.trim();
299        match response.parse::<u32>() {
300            Ok(i) => break i,
301            Err(_) => eprintln!("Invalid Number"),
302        };
303    };
304    client.auth_step_totp(totp).await
305}
306
307async fn do_passkey(
308    client: &mut KanidmClient,
309    pkr: RequestChallengeResponse,
310) -> Result<AuthResponse, ClientError> {
311    let mut wa = get_authenticator();
312    println!("If your authenticator is not attached, attach it now.");
313    println!("Your authenticator will then flash/prompt for confirmation.");
314    #[cfg(target_os = "macos")]
315    println!("Note: TouchID is not currently supported on the CLI 🫤");
316    let auth = wa
317        .do_authentication(client.get_origin().clone(), pkr)
318        .map(Box::new)
319        .unwrap_or_else(|e| {
320            error!("Failed to interact with webauthn device. -- {:?}", e);
321            std::process::exit(1);
322        });
323
324    client.auth_step_passkey_complete(auth).await
325}
326
327async fn do_securitykey(
328    client: &mut KanidmClient,
329    pkr: RequestChallengeResponse,
330) -> Result<AuthResponse, ClientError> {
331    let mut wa = get_authenticator();
332    println!("Your authenticator will now flash for you to interact with it.");
333    let auth = wa
334        .do_authentication(client.get_origin().clone(), pkr)
335        .map(Box::new)
336        .unwrap_or_else(|e| {
337            error!("Failed to interact with webauthn device. -- {:?}", e);
338            std::process::exit(1);
339        });
340
341    client.auth_step_securitykey_complete(auth).await
342}
343
344async fn process_auth_state(
345    mut allowed: Vec<AuthAllowed>,
346    mut client: KanidmClient,
347    maybe_password: &Option<String>,
348    instance_name: &Option<String>,
349) {
350    loop {
351        debug!("Allowed mechanisms -> {:?}", allowed);
352        // What auth can proceed?
353        let choice = match allowed.len() {
354            0 => {
355                error!("Error during authentication phase: Server offered no method to proceed");
356                std::process::exit(1);
357            }
358            1 =>
359            {
360                #[allow(clippy::expect_used)]
361                allowed
362                    .first()
363                    .expect("can not fail - bounds already checked.")
364            }
365            _ => {
366                let mut options = Vec::new();
367                // because we want them in "most secure to least secure" order.
368                allowed.sort_unstable_by(|a, b| Reverse(a).cmp(&Reverse(b)));
369                for val in allowed.iter() {
370                    options.push(val.to_string());
371                }
372                let msg = "Please choose which credential to provide:";
373                let selection = get_index_choice_dialoguer(msg, &options);
374
375                #[allow(clippy::expect_used)]
376                allowed
377                    .get(selection)
378                    .expect("Failed to select an authentication option!")
379            }
380        };
381
382        let res = match choice {
383            AuthAllowed::Anonymous => client.auth_step_anonymous().await,
384            AuthAllowed::Password => do_password(&mut client, maybe_password).await,
385            AuthAllowed::BackupCode => do_backup_code(&mut client).await,
386            AuthAllowed::Totp => do_totp(&mut client).await,
387            AuthAllowed::Passkey(chal) => do_passkey(&mut client, chal.clone()).await,
388            AuthAllowed::SecurityKey(chal) => do_securitykey(&mut client, chal.clone()).await,
389        };
390
391        // Now update state.
392        let state = res
393            .unwrap_or_else(|e| {
394                error!("Error in authentication phase: {:?}", e);
395                std::process::exit(1);
396            })
397            .state;
398
399        // What auth state are we in?
400        allowed = match &state {
401            AuthState::Continue(allowed) => allowed.to_vec(),
402            AuthState::Success(_token) => break,
403            AuthState::Denied(reason) => {
404                error!("Authentication Denied: {:?}", reason);
405                std::process::exit(1);
406            }
407            _ => {
408                error!("Error in authentication phase: invalid authstate");
409                std::process::exit(1);
410            }
411        };
412        // Loop again.
413    }
414
415    // Read the current tokens. If we can't read them, IGNORE!!!
416    let mut tokens = read_tokens(&client.get_token_cache_path()).unwrap_or_default();
417
418    // Select our token instance. Create it if empty.
419    let n_lookup = instance_name.clone().unwrap_or_default();
420    let token_instance = tokens.instances.entry(n_lookup).or_default();
421
422    // Add our new one
423    let (spn, tonk) = match client.get_token().await {
424        Some(t) => {
425            let jwsc = match JwsCompact::from_str(&t) {
426                Ok(j) => j,
427                Err(err) => {
428                    error!(?err, "Unable to parse token");
429                    std::process::exit(1);
430                }
431            };
432
433            let Some(key_id) = jwsc.kid() else {
434                error!("JWS invalid, not key id associated");
435                std::process::exit(1);
436            };
437
438            // Okay, lets check the jwk now.
439            let pub_jwk = if let Some(pub_jwk) = token_instance.keys.get(key_id).cloned() {
440                pub_jwk
441            } else {
442                // Get it from the server.
443                let pub_jwk = match client.get_public_jwk(key_id).await {
444                    Ok(pj) => pj,
445                    Err(err) => {
446                        error!(?err, "Unable to retrieve jwk from server");
447                        std::process::exit(1);
448                    }
449                };
450                token_instance
451                    .keys
452                    .insert(key_id.to_string(), pub_jwk.clone());
453                pub_jwk
454            };
455
456            let jws_verifier = match JwsEs256Verifier::try_from(&pub_jwk) {
457                Ok(verifier) => verifier,
458                Err(err) => {
459                    error!(?err, "Unable to configure jws verifier");
460                    std::process::exit(1);
461                }
462            };
463
464            let tonk = match jws_verifier.verify(&jwsc).and_then(|jws| {
465                jws.from_json::<UserAuthToken>().map_err(|serde_err| {
466                    error!(?serde_err);
467                    JwtError::InvalidJwt
468                })
469            }) {
470                Ok(uat) => uat,
471                Err(err) => {
472                    error!(?err, "Unable to verify token signature");
473                    std::process::exit(1);
474                }
475            };
476
477            let spn = tonk.spn;
478            // Return the original jws
479            (spn, jwsc)
480        }
481        None => {
482            error!("Error retrieving client session");
483            std::process::exit(1);
484        }
485    };
486
487    token_instance.tokens.insert(spn.clone(), tonk);
488
489    // write them out.
490    if write_tokens(&tokens, &client.get_token_cache_path()).is_err() {
491        trace!(?tokens);
492        error!("Error persisting authentication token store");
493        std::process::exit(1);
494    };
495
496    // Success!
497    println!("Login Success for {}", spn);
498}
499
500impl LoginOpt {
501    pub fn debug(&self) -> bool {
502        self.copt.debug
503    }
504
505    pub async fn exec(&self) {
506        let client = self.copt.to_unauth_client();
507        let username = match self.copt.username.as_deref() {
508            Some(val) => val,
509            None => {
510                error!("Please specify a username with -D <USERNAME> to login.");
511                std::process::exit(1);
512            }
513        };
514
515        // What auth mechanisms exist?
516        let mut mechs: Vec<_> = client
517            .auth_step_init(username)
518            .await
519            .unwrap_or_else(|e| {
520                error!("Error during authentication init phase: {:?}", e);
521                std::process::exit(1);
522            })
523            .into_iter()
524            .collect();
525
526        mechs.sort_unstable_by(|a, b| Reverse(a).cmp(&Reverse(b)));
527
528        let mech = match mechs.len() {
529            0 => {
530                error!("Error during authentication init phase: Server offered no authentication mechanisms");
531                std::process::exit(1);
532            }
533            1 =>
534            {
535                #[allow(clippy::expect_used)]
536                mechs
537                    .first()
538                    .expect("can not fail - bounds already checked.")
539            }
540            _ => {
541                let mut options = Vec::new();
542                for val in mechs.iter() {
543                    options.push(val.to_string());
544                }
545                let msg = "Please choose how you want to authenticate:";
546                let selection = get_index_choice_dialoguer(msg, &options);
547
548                #[allow(clippy::expect_used)]
549                mechs
550                    .get(selection)
551                    .expect("can not fail - bounds already checked.")
552            }
553        };
554
555        let allowed = client
556            .auth_step_begin((*mech).clone())
557            .await
558            .unwrap_or_else(|e| {
559                error!("Error during authentication begin phase: {:?}", e);
560                std::process::exit(1);
561            });
562
563        let instance_name = &self.copt.instance;
564
565        // We now have the first auth state, so we can proceed until complete.
566        process_auth_state(allowed, client, &self.password, instance_name).await;
567    }
568}
569
570impl ReauthOpt {
571    pub fn debug(&self) -> bool {
572        self.copt.debug
573    }
574
575    pub(crate) async fn inner(&self, client: KanidmClient) {
576        let instance_name = &self.copt.instance;
577
578        let allowed = client.reauth_begin().await.unwrap_or_else(|e| {
579            error!("Error during reauthentication begin phase: {:?}", e);
580            std::process::exit(1);
581        });
582
583        process_auth_state(allowed, client, &None, instance_name).await;
584    }
585
586    pub async fn exec(&self) {
587        let client = self.copt.to_client(OpType::Read).await;
588        // This is to break a recursion loop in re-auth with to_client
589        self.inner(client).await
590    }
591}
592
593impl LogoutOpt {
594    pub fn debug(&self) -> bool {
595        self.copt.debug
596    }
597
598    pub async fn exec(&self) {
599        let mut tokens = read_tokens(&self.copt.get_token_cache_path()).unwrap_or_else(|_| {
600            error!("Error retrieving authentication token store");
601            std::process::exit(1);
602        });
603
604        let instance_name = &self.copt.instance;
605        let n_lookup = instance_name.clone().unwrap_or_default();
606        let Some(token_instance) = tokens.instances.get_mut(&n_lookup) else {
607            println!("No sessions for instance {}", n_lookup);
608            return;
609        };
610
611        let spn: String = if self.local_only {
612            // For now we just remove this from the token store.
613            let mut _tmp_username = String::new();
614            match &self.copt.username {
615                Some(value) => value.clone(),
616                None => {
617                    // check if we're in a tty
618                    if std::io::stdin().is_terminal() {
619                        match prompt_for_username_get_username(
620                            &self.copt.get_token_cache_path(),
621                            instance_name,
622                        ) {
623                            Ok(value) => value,
624                            Err(msg) => {
625                                error!("{}", msg);
626                                std::process::exit(1);
627                            }
628                        }
629                    } else {
630                        eprintln!("Not running in interactive mode and no username specified, can't continue!");
631                        return;
632                    }
633                }
634            }
635        } else {
636            let client = match self.copt.try_to_client(OpType::Read).await {
637                Ok(c) => c,
638                Err(ToClientError::NeedLogin(_)) => {
639                    // There are no session tokens, so return a success.
640                    std::process::exit(0);
641                }
642                Err(ToClientError::NeedReauth(_, _)) | Err(ToClientError::Other) => {
643                    // This can only occur in bad cases, so fail.
644                    std::process::exit(1);
645                }
646            };
647
648            let token = match client.get_token().await {
649                Some(t) => t,
650                None => {
651                    error!("Client token store is empty/corrupt");
652                    std::process::exit(1);
653                }
654            };
655
656            // Parse it for the SPN. Annoying but it's what we have to do
657            // because we don't know what token was used in the lower to client calls.
658            let jwsc = match JwsCompact::from_str(&token) {
659                Ok(j) => j,
660                Err(err) => {
661                    error!(?err, "Unable to parse token");
662                    info!("The token can be removed locally with `--local-only`");
663                    std::process::exit(1);
664                }
665            };
666
667            let Some(key_id) = jwsc.kid() else {
668                error!("Invalid token, missing KeyID");
669                info!("The token can be removed locally with `--local-only`");
670                std::process::exit(1);
671            };
672
673            let Some(pub_jwk) = token_instance.keys().get(key_id) else {
674                error!("Invalid instance, no signing keys are available");
675                info!("The token can be removed locally with `--local-only`");
676                std::process::exit(1);
677            };
678
679            let jws_verifier = match JwsEs256Verifier::try_from(pub_jwk) {
680                Ok(verifier) => verifier,
681                Err(err) => {
682                    error!(?err, "Unable to configure jws verifier");
683                    info!("The token can be removed locally with `--local-only`");
684                    std::process::exit(1);
685                }
686            };
687
688            let uat = match jws_verifier.verify(&jwsc).and_then(|jws| {
689                jws.from_json::<UserAuthToken>().map_err(|serde_err| {
690                    error!(?serde_err);
691                    info!("The token can be removed locally with `--local-only`");
692                    JwtError::InvalidJwt
693                })
694            }) {
695                Ok(uat) => uat,
696                Err(e) => {
697                    error!(?e, "Unable to verify token signature, may be corrupt");
698                    info!("The token can be removed locally with `--local-only`");
699                    std::process::exit(1);
700                }
701            };
702
703            // Now we know we have a valid(ish) token, call the server to do the logout.
704            if let Err(e) = client.logout().await {
705                error!("Failed to logout - {:?}", e);
706                std::process::exit(1);
707            }
708
709            // Server acked the logout, lets proceed with the local cleanup now, return
710            // the spn so the outer parts know what to remove.
711            uat.spn
712        };
713
714        // Remove our old one
715        if token_instance.tokens.remove(&spn).is_some() {
716            // write them out.
717            if let Err(_e) = write_tokens(&tokens, &self.copt.get_token_cache_path()) {
718                error!("Error persisting authentication token store");
719                std::process::exit(1);
720            };
721            println!("Removed session for {}", spn);
722        } else {
723            println!("No sessions for {}", spn);
724        }
725    }
726}
727
728impl SessionOpt {
729    pub fn debug(&self) -> bool {
730        match self {
731            SessionOpt::List(dopt) | SessionOpt::Cleanup(dopt) => dopt.debug,
732        }
733    }
734
735    pub async fn exec(&self) {
736        match self {
737            SessionOpt::List(copt) => {
738                let token_store = read_tokens(&copt.get_token_cache_path()).unwrap_or_else(|_| {
739                    error!("Error retrieving authentication token store");
740                    std::process::exit(1);
741                });
742
743                let instance_name = &copt.instance;
744
745                let Some(token_instance) = token_store.instances(instance_name) else {
746                    return;
747                };
748
749                for (_, uat) in token_instance.valid_uats() {
750                    println!("---");
751                    println!("{}", uat);
752                }
753            }
754            SessionOpt::Cleanup(copt) => {
755                let mut token_store =
756                    read_tokens(&copt.get_token_cache_path()).unwrap_or_else(|_| {
757                        error!("Error retrieving authentication token store");
758                        std::process::exit(1);
759                    });
760
761                let instance_name = &copt.instance;
762
763                let Some(token_instance) = token_store.instances_mut(instance_name) else {
764                    error!("No tokens for instance");
765                    std::process::exit(1);
766                };
767
768                let now = time::OffsetDateTime::now_utc();
769                let change = token_instance.cleanup(now);
770
771                if let Err(_e) = write_tokens(&token_store, &copt.get_token_cache_path()) {
772                    error!("Error persisting authentication token store");
773                    std::process::exit(1);
774                };
775
776                println!("Removed {} sessions", change);
777            }
778        }
779    }
780}