kanidm_unix_resolver/idprovider/
kanidm.rs

1use super::interface::{
2    tpm::{
3        provider::{BoxedDynTpm, TpmHmacS256},
4        structures::{HmacS256Key, LoadableHmacS256Key, StorageKey},
5    },
6    AuthCredHandler, AuthRequest, AuthResult, GroupToken, GroupTokenState, Id, IdProvider,
7    IdpError, ProviderOrigin, UserToken, UserTokenState,
8};
9use crate::db::KeyStoreTxn;
10use async_trait::async_trait;
11use hashbrown::HashMap;
12use kanidm_client::{ClientError, KanidmClient, StatusCode};
13use kanidm_lib_crypto::CryptoPolicy;
14use kanidm_lib_crypto::DbPasswordV1;
15use kanidm_lib_crypto::Password;
16use kanidm_proto::internal::OperationError;
17use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
18use kanidm_unix_common::unix_config::{GroupMap, KanidmConfig};
19use kanidm_unix_common::unix_proto::PamAuthRequest;
20use std::collections::BTreeSet;
21use std::time::{Duration, SystemTime};
22use tokio::sync::{broadcast, Mutex};
23
24const KANIDM_HMAC_KEY: &str = "kanidm-hmac-key-v2";
25const KANIDM_PWV1_KEY: &str = "kanidm-pw-v1";
26
27// If the provider is offline, we need to backoff and wait a bit.
28const OFFLINE_NEXT_CHECK: Duration = Duration::from_secs(60);
29
30#[derive(Debug, Clone)]
31enum CacheState {
32    Online,
33    Offline,
34    OfflineNextCheck(SystemTime),
35}
36
37struct KanidmProviderInternal {
38    state: CacheState,
39    client: KanidmClient,
40    hmac_key: HmacS256Key,
41    crypto_policy: CryptoPolicy,
42    pam_allow_groups: BTreeSet<String>,
43    bearer_token_set: bool,
44}
45
46pub struct KanidmProvider {
47    inner: Mutex<KanidmProviderInternal>,
48    // Because this value doesn't change, to support fast
49    // lookup we store the extension map here.
50    map_group: HashMap<String, Id>,
51}
52
53impl KanidmProvider {
54    pub async fn new<'a, 'b>(
55        client: KanidmClient,
56        config: &KanidmConfig,
57        now: SystemTime,
58        keystore: &mut KeyStoreTxn<'a, 'b>,
59        tpm: &mut BoxedDynTpm,
60        machine_key: &StorageKey,
61    ) -> Result<Self, IdpError> {
62        let tpm_ctx: &mut dyn TpmHmacS256 = &mut **tpm;
63
64        // Initially retrieve our HMAC key.
65        let loadable_hmac_key: Option<LoadableHmacS256Key> = keystore
66            .get_tagged_hsm_key(KANIDM_HMAC_KEY)
67            .map_err(|ks_err| {
68                error!(?ks_err);
69                IdpError::KeyStore
70            })?;
71
72        let loadable_hmac_key = if let Some(loadable_hmac_key) = loadable_hmac_key {
73            loadable_hmac_key
74        } else {
75            let loadable_hmac_key = tpm_ctx.hmac_s256_create(machine_key).map_err(|tpm_err| {
76                error!(?tpm_err);
77                IdpError::Tpm
78            })?;
79
80            keystore
81                .insert_tagged_hsm_key(KANIDM_HMAC_KEY, &loadable_hmac_key)
82                .map_err(|ks_err| {
83                    error!(?ks_err);
84                    IdpError::KeyStore
85                })?;
86
87            loadable_hmac_key
88        };
89
90        let hmac_key = tpm_ctx
91            .hmac_s256_load(machine_key, &loadable_hmac_key)
92            .map_err(|tpm_err| {
93                error!(?tpm_err);
94                IdpError::Tpm
95            })?;
96
97        let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250));
98
99        let pam_allow_groups = config.pam_allowed_login_groups.iter().cloned().collect();
100
101        let map_group = config
102            .map_group
103            .iter()
104            .cloned()
105            .map(|GroupMap { local, with }| (local, Id::Name(with)))
106            .collect();
107
108        // Set the api token if one is set
109        if let Some(token) = config.service_account_token.clone() {
110            client.set_token(token).await;
111        };
112        let bearer_token_set = config.service_account_token.is_some();
113
114        Ok(KanidmProvider {
115            inner: Mutex::new(KanidmProviderInternal {
116                state: CacheState::OfflineNextCheck(now),
117                client,
118                hmac_key,
119                crypto_policy,
120                pam_allow_groups,
121                bearer_token_set,
122            }),
123            map_group,
124        })
125    }
126}
127
128impl From<UnixUserToken> for UserToken {
129    fn from(value: UnixUserToken) -> UserToken {
130        let UnixUserToken {
131            name,
132            spn,
133            displayname,
134            gidnumber,
135            uuid,
136            shell,
137            groups,
138            sshkeys,
139            valid,
140        } = value;
141
142        let sshkeys = sshkeys.iter().map(|s| s.to_string()).collect();
143
144        let groups = groups.into_iter().map(GroupToken::from).collect();
145
146        UserToken {
147            provider: ProviderOrigin::Kanidm,
148            name,
149            spn,
150            uuid,
151            gidnumber,
152            displayname,
153            shell,
154            groups,
155            sshkeys,
156            valid,
157            extra_keys: Default::default(),
158        }
159    }
160}
161
162impl From<UnixGroupToken> for GroupToken {
163    fn from(value: UnixGroupToken) -> GroupToken {
164        let UnixGroupToken {
165            name,
166            spn,
167            uuid,
168            gidnumber,
169        } = value;
170
171        GroupToken {
172            provider: ProviderOrigin::Kanidm,
173            name,
174            spn,
175            uuid,
176            gidnumber,
177            extra_keys: Default::default(),
178        }
179    }
180}
181
182impl UserToken {
183    pub fn kanidm_update_cached_password(
184        &mut self,
185        crypto_policy: &CryptoPolicy,
186        cred: &str,
187        tpm: &mut BoxedDynTpm,
188        hmac_key: &HmacS256Key,
189    ) {
190        let tpm_ctx: &mut dyn TpmHmacS256 = &mut **tpm;
191
192        let pw = match Password::new_argon2id_hsm(crypto_policy, cred, tpm_ctx, hmac_key) {
193            Ok(pw) => pw,
194            Err(reason) => {
195                // Clear cached pw.
196                self.extra_keys.remove(KANIDM_PWV1_KEY);
197                warn!(
198                    ?reason,
199                    "unable to apply kdf to password, clearing cached password."
200                );
201                return;
202            }
203        };
204
205        let pw_value = match serde_json::to_value(pw.to_dbpasswordv1()) {
206            Ok(pw) => pw,
207            Err(reason) => {
208                // Clear cached pw.
209                self.extra_keys.remove(KANIDM_PWV1_KEY);
210                warn!(
211                    ?reason,
212                    "unable to serialise credential, clearing cached password."
213                );
214                return;
215            }
216        };
217
218        self.extra_keys.insert(KANIDM_PWV1_KEY.into(), pw_value);
219        debug!(spn = %self.spn, "Updated cached pw");
220    }
221
222    pub fn kanidm_check_cached_password(
223        &self,
224        cred: &str,
225        tpm: &mut BoxedDynTpm,
226        hmac_key: &HmacS256Key,
227    ) -> bool {
228        let pw_value = match self.extra_keys.get(KANIDM_PWV1_KEY) {
229            Some(pw_value) => pw_value,
230            None => {
231                debug!(spn = %self.spn, "no cached pw available");
232                return false;
233            }
234        };
235
236        let dbpw = match serde_json::from_value::<DbPasswordV1>(pw_value.clone()) {
237            Ok(dbpw) => dbpw,
238            Err(reason) => {
239                warn!(spn = %self.spn, ?reason, "unable to deserialise credential");
240                return false;
241            }
242        };
243
244        let pw = match Password::try_from(dbpw) {
245            Ok(pw) => pw,
246            Err(reason) => {
247                warn!(spn = %self.spn, ?reason, "unable to process credential");
248                return false;
249            }
250        };
251
252        let tpm_ctx: &mut dyn TpmHmacS256 = &mut **tpm;
253
254        pw.verify_ctx(cred, Some((tpm_ctx, hmac_key)))
255            .unwrap_or_default()
256    }
257}
258
259impl KanidmProviderInternal {
260    #[instrument(level = "debug", skip_all)]
261    async fn check_online(&mut self, tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
262        match self.state {
263            // Proceed
264            CacheState::Online => true,
265            CacheState::OfflineNextCheck(at_time) if now >= at_time => {
266                self.attempt_online(tpm, now).await
267            }
268            CacheState::OfflineNextCheck(_) | CacheState::Offline => false,
269        }
270    }
271
272    #[instrument(level = "debug", skip_all)]
273    async fn check_online_right_meow(&mut self, tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
274        match self.state {
275            CacheState::Online => true,
276            CacheState::OfflineNextCheck(_) => self.attempt_online(tpm, now).await,
277            CacheState::Offline => false,
278        }
279    }
280
281    #[instrument(level = "debug", skip_all)]
282    async fn attempt_online(&mut self, _tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
283        let mut max_attempts = 3;
284        while max_attempts > 0 {
285            max_attempts -= 1;
286
287            // If a bearer token is set, we don't want to do an auth flow and
288            // remove that. Just do a whoami call, which will tell us the result.
289            let check_online_result = if self.bearer_token_set {
290                self.client.whoami().await.map(|_| ())
291            } else {
292                self.client.auth_anonymous().await
293            };
294
295            match check_online_result {
296                Ok(_uat) => {
297                    debug!("provider is now online");
298                    self.state = CacheState::Online;
299                    return true;
300                }
301                Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
302                    error!(?reason, ?opid, "Provider authentication returned unauthorized, {max_attempts} attempts remaining.");
303                    // Provider needs to re-auth ASAP. We set this state value here
304                    // so that if we exceed max attempts, the next caller knows to check
305                    // online immediately.
306                    self.state = CacheState::OfflineNextCheck(now);
307                    // attempt again immediately!!!!
308                    continue;
309                }
310                Err(err) => {
311                    error!(?err, "Provider online failed");
312                    self.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
313                    return false;
314                }
315            }
316        }
317        warn!("Exceeded maximum number of attempts to bring provider online");
318        return false;
319    }
320}
321
322#[async_trait]
323impl IdProvider for KanidmProvider {
324    fn origin(&self) -> ProviderOrigin {
325        ProviderOrigin::Kanidm
326    }
327
328    async fn attempt_online(&self, tpm: &mut BoxedDynTpm, now: SystemTime) -> bool {
329        let mut inner = self.inner.lock().await;
330        inner.check_online_right_meow(tpm, now).await
331    }
332
333    async fn mark_next_check(&self, now: SystemTime) {
334        let mut inner = self.inner.lock().await;
335        inner.state = CacheState::OfflineNextCheck(now);
336    }
337
338    fn has_map_group(&self, local: &str) -> Option<&Id> {
339        self.map_group.get(local)
340    }
341
342    async fn mark_offline(&self) {
343        let mut inner = self.inner.lock().await;
344        inner.state = CacheState::Offline;
345    }
346
347    async fn unix_user_get(
348        &self,
349        id: &Id,
350        token: Option<&UserToken>,
351        tpm: &mut BoxedDynTpm,
352        now: SystemTime,
353    ) -> Result<UserTokenState, IdpError> {
354        let mut inner = self.inner.lock().await;
355
356        if !inner.check_online(tpm, now).await {
357            // We are offline, return that we should use a cached token.
358            return Ok(UserTokenState::UseCached);
359        }
360
361        // We are ONLINE, do the get.
362        match inner
363            .client
364            .idm_account_unix_token_get(id.to_string().as_str())
365            .await
366        {
367            Ok(tok) => {
368                let mut ut = UserToken::from(tok);
369
370                if let Some(previous_token) = token {
371                    ut.extra_keys = previous_token.extra_keys.clone();
372                }
373
374                Ok(UserTokenState::Update(ut))
375            }
376            // Offline?
377            Err(ClientError::Transport(err)) => {
378                error!(?err, "transport error");
379                inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
380                Ok(UserTokenState::UseCached)
381            }
382            // Provider session error, need to re-auth
383            Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
384                match reason {
385                    Some(OperationError::NotAuthenticated) => warn!(
386                        "session not authenticated - attempting reauthentication - eventid {}",
387                        opid
388                    ),
389                    Some(OperationError::SessionExpired) => warn!(
390                        "session expired - attempting reauthentication - eventid {}",
391                        opid
392                    ),
393                    e => error!(
394                        "authentication error {:?}, moving to offline - eventid {}",
395                        e, opid
396                    ),
397                };
398                // Provider needs to re-auth ASAP
399                inner.state = CacheState::OfflineNextCheck(now);
400                Ok(UserTokenState::UseCached)
401            }
402            // 404 / Removed.
403            Err(ClientError::Http(
404                StatusCode::BAD_REQUEST,
405                Some(OperationError::NoMatchingEntries),
406                opid,
407            ))
408            | Err(ClientError::Http(
409                StatusCode::NOT_FOUND,
410                Some(OperationError::NoMatchingEntries),
411                opid,
412            ))
413            | Err(ClientError::Http(
414                StatusCode::NOT_FOUND,
415                Some(OperationError::MissingAttribute(_)),
416                opid,
417            ))
418            | Err(ClientError::Http(
419                StatusCode::NOT_FOUND,
420                Some(OperationError::MissingClass(_)),
421                opid,
422            ))
423            | Err(ClientError::Http(
424                StatusCode::BAD_REQUEST,
425                Some(OperationError::InvalidAccountState(_)),
426                opid,
427            )) => {
428                debug!(
429                    ?opid,
430                    "entry has been removed or is no longer a valid posix account"
431                );
432                Ok(UserTokenState::NotFound)
433            }
434            // Something is really wrong? We did get a response though, so we are still online.
435            Err(err) => {
436                error!(?err, "client error");
437                Err(IdpError::BadRequest)
438            }
439        }
440    }
441
442    async fn unix_user_online_auth_init(
443        &self,
444        _account_id: &str,
445        _token: &UserToken,
446        _tpm: &mut BoxedDynTpm,
447        _shutdown_rx: &broadcast::Receiver<()>,
448    ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
449        // Not sure that I need to do much here?
450        Ok((AuthRequest::Password, AuthCredHandler::Password))
451    }
452
453    async fn unix_unknown_user_online_auth_init(
454        &self,
455        _account_id: &str,
456        _tpm: &mut BoxedDynTpm,
457        _shutdown_rx: &broadcast::Receiver<()>,
458    ) -> Result<Option<(AuthRequest, AuthCredHandler)>, IdpError> {
459        // We do not support unknown user auth.
460        Ok(None)
461    }
462
463    async fn unix_user_online_auth_step(
464        &self,
465        account_id: &str,
466        current_token: Option<&UserToken>,
467        cred_handler: &mut AuthCredHandler,
468        pam_next_req: PamAuthRequest,
469        tpm: &mut BoxedDynTpm,
470        _shutdown_rx: &broadcast::Receiver<()>,
471    ) -> Result<AuthResult, IdpError> {
472        match (cred_handler, pam_next_req) {
473            (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
474                let inner = self.inner.lock().await;
475
476                let auth_result = inner
477                    .client
478                    .idm_account_unix_cred_verify(account_id, &cred)
479                    .await;
480
481                trace!(?auth_result);
482
483                match auth_result {
484                    Ok(Some(n_tok)) => {
485                        let mut new_token = UserToken::from(n_tok);
486
487                        // Update any keys that may have been in the db in the current
488                        // token.
489                        if let Some(previous_token) = current_token {
490                            new_token.extra_keys = previous_token.extra_keys.clone();
491                        }
492
493                        // Set any new keys that are relevant from this authentication
494                        new_token.kanidm_update_cached_password(
495                            &inner.crypto_policy,
496                            cred.as_str(),
497                            tpm,
498                            &inner.hmac_key,
499                        );
500
501                        Ok(AuthResult::SuccessUpdate { new_token })
502                    }
503                    Ok(None) => {
504                        // TODO: i'm not a huge fan of this rn, but currently the way we handle
505                        // an expired account is we return Ok(None).
506                        //
507                        // We can't tell the difference between expired and incorrect password.
508                        // So in these cases we have to clear the cached password. :(
509                        //
510                        // In future once we have domain join, we should be getting the user token
511                        // at the start of the auth and checking for account validity instead.
512                        Ok(AuthResult::Denied)
513                    }
514                    Err(ClientError::Transport(err)) => {
515                        error!(?err, "A client transport error occured.");
516                        Err(IdpError::Transport)
517                    }
518                    Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
519                        match reason {
520                            Some(OperationError::NotAuthenticated) => warn!(
521                                "session not authenticated - attempting reauthentication - eventid {}",
522                                opid
523                            ),
524                            Some(OperationError::SessionExpired) => warn!(
525                                "session expired - attempting reauthentication - eventid {}",
526                                opid
527                            ),
528                            e => error!(
529                                "authentication error {:?}, moving to offline - eventid {}",
530                                e, opid
531                            ),
532                        };
533                        Err(IdpError::ProviderUnauthorised)
534                    }
535                    Err(ClientError::Http(
536                        StatusCode::BAD_REQUEST,
537                        Some(OperationError::NoMatchingEntries),
538                        opid,
539                    ))
540                    | Err(ClientError::Http(
541                        StatusCode::NOT_FOUND,
542                        Some(OperationError::NoMatchingEntries),
543                        opid,
544                    ))
545                    | Err(ClientError::Http(
546                        StatusCode::NOT_FOUND,
547                        Some(OperationError::MissingAttribute(_)),
548                        opid,
549                    ))
550                    | Err(ClientError::Http(
551                        StatusCode::NOT_FOUND,
552                        Some(OperationError::MissingClass(_)),
553                        opid,
554                    ))
555                    | Err(ClientError::Http(
556                        StatusCode::BAD_REQUEST,
557                        Some(OperationError::InvalidAccountState(_)),
558                        opid,
559                    )) => {
560                        error!(
561                            "unknown account or is not a valid posix account - eventid {}",
562                            opid
563                        );
564                        Err(IdpError::NotFound)
565                    }
566                    Err(err) => {
567                        error!(?err, "client error");
568                        // Some other unknown processing error?
569                        Err(IdpError::BadRequest)
570                    }
571                }
572            }
573            (
574                AuthCredHandler::DeviceAuthorizationGrant,
575                PamAuthRequest::DeviceAuthorizationGrant { .. },
576            ) => {
577                error!("DeviceAuthorizationGrant not implemented!");
578                Err(IdpError::BadRequest)
579            }
580            _ => {
581                error!("invalid authentication request state");
582                Err(IdpError::BadRequest)
583            }
584        }
585    }
586
587    async fn unix_user_offline_auth_init(
588        &self,
589        _token: &UserToken,
590    ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
591        Ok((AuthRequest::Password, AuthCredHandler::Password))
592    }
593
594    async fn unix_user_offline_auth_step(
595        &self,
596        current_token: Option<&UserToken>,
597        session_token: &UserToken,
598        cred_handler: &mut AuthCredHandler,
599        pam_next_req: PamAuthRequest,
600        tpm: &mut BoxedDynTpm,
601    ) -> Result<AuthResult, IdpError> {
602        match (cred_handler, pam_next_req) {
603            (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
604                let inner = self.inner.lock().await;
605
606                if session_token.kanidm_check_cached_password(cred.as_str(), tpm, &inner.hmac_key) {
607                    // Ensure we have either the latest token, or if none, at least the session token.
608                    let new_token = current_token.unwrap_or(session_token).clone();
609
610                    // TODO: We can update the token here and then do lockouts.
611
612                    Ok(AuthResult::SuccessUpdate { new_token })
613                } else {
614                    Ok(AuthResult::Denied)
615                }
616            }
617            (
618                AuthCredHandler::DeviceAuthorizationGrant,
619                PamAuthRequest::DeviceAuthorizationGrant { .. },
620            ) => {
621                error!("DeviceAuthorizationGrant not implemented!");
622                Err(IdpError::BadRequest)
623            }
624            _ => {
625                error!("invalid authentication request state");
626                Err(IdpError::BadRequest)
627            }
628        }
629    }
630
631    async fn unix_group_get(
632        &self,
633        id: &Id,
634        tpm: &mut BoxedDynTpm,
635        now: SystemTime,
636    ) -> Result<GroupTokenState, IdpError> {
637        let mut inner = self.inner.lock().await;
638
639        if !inner.check_online(tpm, now).await {
640            // We are offline, return that we should use a cached token.
641            return Ok(GroupTokenState::UseCached);
642        }
643
644        match inner
645            .client
646            .idm_group_unix_token_get(id.to_string().as_str())
647            .await
648        {
649            Ok(tok) => {
650                let gt = GroupToken::from(tok);
651                Ok(GroupTokenState::Update(gt))
652            }
653            // Offline?
654            Err(ClientError::Transport(err)) => {
655                error!(?err, "transport error");
656                inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
657                Ok(GroupTokenState::UseCached)
658            }
659            // Provider session error, need to re-auth
660            Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
661                match reason {
662                    Some(OperationError::NotAuthenticated) => warn!(
663                        "session not authenticated - attempting reauthentication - eventid {}",
664                        opid
665                    ),
666                    Some(OperationError::SessionExpired) => warn!(
667                        "session expired - attempting reauthentication - eventid {}",
668                        opid
669                    ),
670                    e => error!(
671                        "authentication error {:?}, moving to offline - eventid {}",
672                        e, opid
673                    ),
674                };
675                inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
676                Ok(GroupTokenState::UseCached)
677            }
678            // 404 / Removed.
679            Err(ClientError::Http(
680                StatusCode::BAD_REQUEST,
681                Some(OperationError::NoMatchingEntries),
682                opid,
683            ))
684            | Err(ClientError::Http(
685                StatusCode::NOT_FOUND,
686                Some(OperationError::NoMatchingEntries),
687                opid,
688            ))
689            | Err(ClientError::Http(
690                StatusCode::NOT_FOUND,
691                Some(OperationError::MissingAttribute(_)),
692                opid,
693            ))
694            | Err(ClientError::Http(
695                StatusCode::NOT_FOUND,
696                Some(OperationError::MissingClass(_)),
697                opid,
698            ))
699            | Err(ClientError::Http(
700                StatusCode::BAD_REQUEST,
701                Some(OperationError::InvalidAccountState(_)),
702                opid,
703            )) => {
704                debug!(
705                    ?opid,
706                    "entry has been removed or is no longer a valid posix account"
707                );
708                Ok(GroupTokenState::NotFound)
709            }
710            // Something is really wrong? We did get a response though, so we are still online.
711            Err(err) => {
712                error!(?err, "client error");
713                Err(IdpError::BadRequest)
714            }
715        }
716    }
717
718    async fn unix_user_authorise(&self, token: &UserToken) -> Result<Option<bool>, IdpError> {
719        let inner = self.inner.lock().await;
720
721        if inner.pam_allow_groups.is_empty() {
722            // can't allow anything if the group list is zero...
723            warn!("NO USERS CAN LOGIN TO THIS SYSTEM! There are no `pam_allowed_login_groups` in configuration!");
724            Ok(Some(false))
725        } else {
726            let user_set: BTreeSet<_> = token
727                .groups
728                .iter()
729                .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()])
730                .collect();
731
732            debug!(
733                "Checking if user is in allowed groups ({:?}) -> {:?}",
734                inner.pam_allow_groups, user_set,
735            );
736            let intersection_count = user_set.intersection(&inner.pam_allow_groups).count();
737            debug!("Number of intersecting groups: {}", intersection_count);
738            debug!("User token is valid: {}", token.valid);
739
740            if intersection_count == 0 && token.valid {
741                warn!("The user {} authenticated successfully but is NOT a member of a group defined in `pam_allowed_login_groups`. They have been denied access to this system.", token.spn);
742            }
743
744            Ok(Some(intersection_count > 0 && token.valid))
745        }
746    }
747}