kanidm_unix_resolver/idprovider/
kanidm.rs

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