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