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