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
27const 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 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 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 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 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 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 self.state = CacheState::OfflineNextCheck(now);
290 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 return Ok(UserTokenState::UseCached);
342 }
343
344 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 Err(ClientError::Transport(err)) => {
361 error!(?err, "transport error");
362 inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
363 Ok(UserTokenState::UseCached)
364 }
365 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 inner.state = CacheState::OfflineNextCheck(now);
383 Ok(UserTokenState::UseCached)
384 }
385 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 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 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 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 if let Some(previous_token) = current_token {
473 new_token.extra_keys = previous_token.extra_keys.clone();
474 }
475
476 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 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 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 let new_token = current_token.unwrap_or(session_token).clone();
592
593 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 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 Err(ClientError::Transport(err)) => {
638 error!(?err, "transport error");
639 inner.state = CacheState::OfflineNextCheck(now + OFFLINE_NEXT_CHECK);
640 Ok(GroupTokenState::UseCached)
641 }
642 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 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 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 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}