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    #[instrument(level = "debug", skip_all, fields(id = ?id))]
348    async fn unix_user_get(
349        &self,
350        id: &Id,
351        token: Option<&UserToken>,
352        tpm: &mut BoxedDynTpm,
353        now: SystemTime,
354    ) -> Result<UserTokenState, IdpError> {
355        let mut inner = self.inner.lock().await;
356
357        if !inner.check_online(tpm, now).await {
358            // We are offline, return that we should use a cached token.
359            return Ok(UserTokenState::UseCached);
360        }
361
362        // We are ONLINE, do the get.
363        match inner
364            .client
365            .idm_account_unix_token_get(id.to_string().as_str())
366            .await
367        {
368            Ok(tok) => {
369                let mut ut = UserToken::from(tok);
370
371                if let Some(previous_token) = token {
372                    ut.extra_keys = previous_token.extra_keys.clone();
373                }
374
375                Ok(UserTokenState::Update(ut))
376            }
377            // Offline?
378            Err(ClientError::Transport(err)) => {
379                error!(?err, "transport error");
380                inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
381                Ok(UserTokenState::UseCached)
382            }
383            // Provider session error, need to re-auth
384            Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
385                match reason {
386                    Some(OperationError::NotAuthenticated) => warn!(
387                        "session not authenticated - attempting reauthentication - eventid {}",
388                        opid
389                    ),
390                    Some(OperationError::SessionExpired) => warn!(
391                        "session expired - attempting reauthentication - eventid {}",
392                        opid
393                    ),
394                    e => error!(
395                        "authentication error {:?}, moving to offline - eventid {}",
396                        e, opid
397                    ),
398                };
399                // Provider needs to re-auth ASAP
400                inner.state = CacheState::OfflineNextCheck(now);
401                Ok(UserTokenState::UseCached)
402            }
403            // 404 / Removed.
404            Err(ClientError::Http(
405                StatusCode::BAD_REQUEST,
406                Some(OperationError::NoMatchingEntries),
407                opid,
408            ))
409            | Err(ClientError::Http(
410                StatusCode::NOT_FOUND,
411                Some(OperationError::NoMatchingEntries),
412                opid,
413            ))
414            | Err(ClientError::Http(
415                StatusCode::NOT_FOUND,
416                Some(OperationError::MissingAttribute(_)),
417                opid,
418            ))
419            | Err(ClientError::Http(
420                StatusCode::NOT_FOUND,
421                Some(OperationError::MissingClass(_)),
422                opid,
423            ))
424            | Err(ClientError::Http(
425                StatusCode::BAD_REQUEST,
426                Some(OperationError::InvalidAccountState(_)),
427                opid,
428            )) => {
429                debug!(
430                    ?opid,
431                    "entry has been removed or is no longer a valid posix account"
432                );
433                Ok(UserTokenState::NotFound)
434            }
435            // Something is really wrong? We did get a response though, so we are still online.
436            Err(err) => {
437                error!(?err, "client error");
438                Err(IdpError::BadRequest)
439            }
440        }
441    }
442
443    #[instrument(level = "debug", skip_all)]
444    async fn unix_user_online_auth_init(
445        &self,
446        _account_id: &str,
447        _token: &UserToken,
448        _tpm: &mut BoxedDynTpm,
449        _shutdown_rx: &broadcast::Receiver<()>,
450    ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
451        // Not sure that I need to do much here?
452        Ok((AuthRequest::Password, AuthCredHandler::Password))
453    }
454
455    async fn unix_unknown_user_online_auth_init(
456        &self,
457        _account_id: &str,
458        _tpm: &mut BoxedDynTpm,
459        _shutdown_rx: &broadcast::Receiver<()>,
460    ) -> Result<Option<(AuthRequest, AuthCredHandler)>, IdpError> {
461        // We do not support unknown user auth.
462        Ok(None)
463    }
464
465    #[instrument(level = "debug", skip_all)]
466    async fn unix_user_online_auth_step(
467        &self,
468        account_id: &str,
469        current_token: Option<&UserToken>,
470        cred_handler: &mut AuthCredHandler,
471        pam_next_req: PamAuthRequest,
472        tpm: &mut BoxedDynTpm,
473        _shutdown_rx: &broadcast::Receiver<()>,
474    ) -> Result<AuthResult, IdpError> {
475        match (cred_handler, pam_next_req) {
476            (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
477                let inner = self.inner.lock().await;
478
479                let auth_result = inner
480                    .client
481                    .idm_account_unix_cred_verify(account_id, &cred)
482                    .await;
483
484                trace!(?auth_result);
485
486                match auth_result {
487                    Ok(Some(n_tok)) => {
488                        let mut new_token = UserToken::from(n_tok);
489
490                        // Update any keys that may have been in the db in the current
491                        // token.
492                        if let Some(previous_token) = current_token {
493                            new_token.extra_keys = previous_token.extra_keys.clone();
494                        }
495
496                        // Set any new keys that are relevant from this authentication
497                        new_token.kanidm_update_cached_password(
498                            &inner.crypto_policy,
499                            cred.as_str(),
500                            tpm,
501                            &inner.hmac_key,
502                        );
503
504                        Ok(AuthResult::SuccessUpdate { new_token })
505                    }
506                    Ok(None) => {
507                        // TODO: i'm not a huge fan of this rn, but currently the way we handle
508                        // an expired account is we return Ok(None).
509                        //
510                        // We can't tell the difference between expired and incorrect password.
511                        // So in these cases we have to clear the cached password. :(
512                        //
513                        // In future once we have domain join, we should be getting the user token
514                        // at the start of the auth and checking for account validity instead.
515                        Ok(AuthResult::Denied)
516                    }
517                    Err(ClientError::Transport(err)) => {
518                        error!(?err, "A client transport error occurred.");
519                        Err(IdpError::Transport)
520                    }
521                    Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
522                        match reason {
523                            Some(OperationError::NotAuthenticated) => warn!(
524                                "session not authenticated - attempting reauthentication - eventid {}",
525                                opid
526                            ),
527                            Some(OperationError::SessionExpired) => warn!(
528                                "session expired - attempting reauthentication - eventid {}",
529                                opid
530                            ),
531                            e => error!(
532                                "authentication error {:?}, moving to offline - eventid {}",
533                                e, opid
534                            ),
535                        };
536                        Err(IdpError::ProviderUnauthorised)
537                    }
538                    Err(ClientError::Http(
539                        StatusCode::BAD_REQUEST,
540                        Some(OperationError::NoMatchingEntries),
541                        opid,
542                    ))
543                    | Err(ClientError::Http(
544                        StatusCode::NOT_FOUND,
545                        Some(OperationError::NoMatchingEntries),
546                        opid,
547                    ))
548                    | Err(ClientError::Http(
549                        StatusCode::NOT_FOUND,
550                        Some(OperationError::MissingAttribute(_)),
551                        opid,
552                    ))
553                    | Err(ClientError::Http(
554                        StatusCode::NOT_FOUND,
555                        Some(OperationError::MissingClass(_)),
556                        opid,
557                    ))
558                    | Err(ClientError::Http(
559                        StatusCode::BAD_REQUEST,
560                        Some(OperationError::InvalidAccountState(_)),
561                        opid,
562                    )) => {
563                        error!(
564                            "unknown account or is not a valid posix account - eventid {}",
565                            opid
566                        );
567                        Err(IdpError::NotFound)
568                    }
569                    Err(err) => {
570                        error!(?err, "client error");
571                        // Some other unknown processing error?
572                        Err(IdpError::BadRequest)
573                    }
574                }
575            }
576            (
577                AuthCredHandler::DeviceAuthorizationGrant,
578                PamAuthRequest::DeviceAuthorizationGrant { .. },
579            ) => {
580                error!("DeviceAuthorizationGrant not implemented!");
581                Err(IdpError::BadRequest)
582            }
583            _ => {
584                error!("invalid authentication request state");
585                Err(IdpError::BadRequest)
586            }
587        }
588    }
589
590    async fn unix_user_offline_auth_init(
591        &self,
592        _token: &UserToken,
593    ) -> Result<(AuthRequest, AuthCredHandler), IdpError> {
594        Ok((AuthRequest::Password, AuthCredHandler::Password))
595    }
596
597    async fn unix_user_offline_auth_step(
598        &self,
599        current_token: Option<&UserToken>,
600        session_token: &UserToken,
601        cred_handler: &mut AuthCredHandler,
602        pam_next_req: PamAuthRequest,
603        tpm: &mut BoxedDynTpm,
604    ) -> Result<AuthResult, IdpError> {
605        match (cred_handler, pam_next_req) {
606            (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
607                let inner = self.inner.lock().await;
608
609                if session_token.kanidm_check_cached_password(cred.as_str(), tpm, &inner.hmac_key) {
610                    // Ensure we have either the latest token, or if none, at least the session token.
611                    let new_token = current_token.unwrap_or(session_token).clone();
612
613                    // TODO: We can update the token here and then do lockouts.
614
615                    Ok(AuthResult::SuccessUpdate { new_token })
616                } else {
617                    Ok(AuthResult::Denied)
618                }
619            }
620            (
621                AuthCredHandler::DeviceAuthorizationGrant,
622                PamAuthRequest::DeviceAuthorizationGrant { .. },
623            ) => {
624                error!("DeviceAuthorizationGrant not implemented!");
625                Err(IdpError::BadRequest)
626            }
627            _ => {
628                error!("invalid authentication request state");
629                Err(IdpError::BadRequest)
630            }
631        }
632    }
633
634    #[instrument(level = "debug", skip_all)]
635    async fn unix_group_get(
636        &self,
637        id: &Id,
638        tpm: &mut BoxedDynTpm,
639        now: SystemTime,
640    ) -> Result<GroupTokenState, IdpError> {
641        let mut inner = self.inner.lock().await;
642
643        if !inner.check_online(tpm, now).await {
644            // We are offline, return that we should use a cached token.
645            return Ok(GroupTokenState::UseCached);
646        }
647
648        match inner
649            .client
650            .idm_group_unix_token_get(id.to_string().as_str())
651            .await
652        {
653            Ok(tok) => {
654                let gt = GroupToken::from(tok);
655                Ok(GroupTokenState::Update(gt))
656            }
657            // Offline?
658            Err(ClientError::Transport(err)) => {
659                error!(?err, "transport error");
660                inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
661                Ok(GroupTokenState::UseCached)
662            }
663            // Provider session error, need to re-auth
664            Err(ClientError::Http(StatusCode::UNAUTHORIZED, reason, opid)) => {
665                match reason {
666                    Some(OperationError::NotAuthenticated) => warn!(
667                        "session not authenticated - attempting reauthentication - eventid {}",
668                        opid
669                    ),
670                    Some(OperationError::SessionExpired) => warn!(
671                        "session expired - attempting reauthentication - eventid {}",
672                        opid
673                    ),
674                    e => error!(
675                        "authentication error {:?}, moving to offline - eventid {}",
676                        e, opid
677                    ),
678                };
679                inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
680                Ok(GroupTokenState::UseCached)
681            }
682            // 404 / Removed.
683            Err(ClientError::Http(
684                StatusCode::BAD_REQUEST,
685                Some(OperationError::NoMatchingEntries),
686                opid,
687            ))
688            | Err(ClientError::Http(
689                StatusCode::NOT_FOUND,
690                Some(OperationError::NoMatchingEntries),
691                opid,
692            ))
693            | Err(ClientError::Http(
694                StatusCode::NOT_FOUND,
695                Some(OperationError::MissingAttribute(_)),
696                opid,
697            ))
698            | Err(ClientError::Http(
699                StatusCode::NOT_FOUND,
700                Some(OperationError::MissingClass(_)),
701                opid,
702            ))
703            | Err(ClientError::Http(
704                StatusCode::BAD_REQUEST,
705                Some(OperationError::InvalidAccountState(_)),
706                opid,
707            )) => {
708                debug!(
709                    ?opid,
710                    "entry has been removed or is no longer a valid posix account"
711                );
712                Ok(GroupTokenState::NotFound)
713            }
714            // Something is really wrong? We did get a response though, so we are still online.
715            Err(err) => {
716                error!(?err, "client error");
717                Err(IdpError::BadRequest)
718            }
719        }
720    }
721
722    async fn unix_user_authorise(&self, token: &UserToken) -> Result<Option<bool>, IdpError> {
723        let inner = self.inner.lock().await;
724
725        if inner.pam_allow_groups.is_empty() {
726            // can't allow anything if the group list is zero...
727            warn!("NO USERS CAN LOGIN TO THIS SYSTEM! There are no `pam_allowed_login_groups` in configuration!");
728            Ok(Some(false))
729        } else {
730            let user_set: BTreeSet<_> = token
731                .groups
732                .iter()
733                .flat_map(|g| [g.name.clone(), g.uuid.hyphenated().to_string()])
734                .collect();
735
736            debug!(
737                "Checking if user is in allowed groups ({:?}) -> {:?}",
738                inner.pam_allow_groups, user_set,
739            );
740            let intersection_count = user_set.intersection(&inner.pam_allow_groups).count();
741            debug!("Number of intersecting groups: {}", intersection_count);
742            debug!("User token is valid: {}", token.valid);
743
744            if intersection_count == 0 && token.valid {
745                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);
746            }
747
748            Ok(Some(intersection_count > 0 && token.valid))
749        }
750    }
751}