1use crate::event::ModifyEvent;
11use crate::plugins::Plugin;
12use crate::prelude::*;
13use crate::value::SessionState;
14use std::collections::BTreeSet;
15use std::sync::Arc;
16use time::OffsetDateTime;
17
18pub struct SessionConsistency {}
19
20impl Plugin for SessionConsistency {
21    fn id() -> &'static str {
22        "plugin_session_consistency"
23    }
24
25    #[instrument(level = "debug", name = "session_consistency", skip_all)]
26    fn pre_modify(
27        qs: &mut QueryServerWriteTransaction,
28        _pre_cand: &[Arc<EntrySealedCommitted>],
29        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
30        _me: &ModifyEvent,
31    ) -> Result<(), OperationError> {
32        Self::modify_inner(qs, cand)
33    }
34
35    #[instrument(level = "debug", name = "session_consistency", skip_all)]
36    fn pre_batch_modify(
37        qs: &mut QueryServerWriteTransaction,
38        _pre_cand: &[Arc<EntrySealedCommitted>],
39        cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
40        _me: &BatchModifyEvent,
41    ) -> Result<(), OperationError> {
42        Self::modify_inner(qs, cand)
43    }
44}
45
46impl SessionConsistency {
47    fn modify_inner<T: Clone + std::fmt::Debug>(
48        qs: &mut QueryServerWriteTransaction,
49        cand: &mut [Entry<EntryInvalid, T>],
50    ) -> Result<(), OperationError> {
51        let curtime = qs.get_curtime();
52        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
53        trace!(%curtime_odt);
54
55        cand.iter_mut().try_for_each(|entry| {
57            let cred_ids: BTreeSet<Uuid> =
59                entry
60                    .get_ava_single_credential(Attribute::PrimaryCredential)
61                        .iter()
62                        .map(|c| c.uuid)
63
64                .chain(
65                    entry.get_ava_passkeys(Attribute::PassKeys)
66                        .iter()
67                        .flat_map(|pks| pks.keys().copied())
68                )
69                .chain(
70                    entry.get_ava_attestedpasskeys(Attribute::AttestedPasskeys)
71                        .iter()
72                        .flat_map(|pks| pks.keys().copied())
73                )
74                .collect();
75
76            let invalidate: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
77                .map(|sessions| {
78                    sessions.iter().filter_map(|(session_id, session)| {
79                        match &session.state {
80                            SessionState::RevokedAt(_) => {
81                                None
83                            }
84                            SessionState::ExpiresAt(_) |
85                            SessionState::NeverExpires =>
86                                if !cred_ids.contains(&session.cred_id) {
87                                    info!(%session_id, "Revoking auth session whose issuing credential no longer exists");
88                                    Some(PartialValue::Refer(*session_id))
89                                } else {
90                                    None
91                                },
92                        }
93                    })
94                    .collect()
95                });
96
97            if let Some(invalidate) = invalidate.as_ref() {
98                entry.remove_avas(Attribute::UserAuthTokenSession, invalidate);
99            }
100
101            let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
103                .map(|sessions| {
104                    sessions.iter().filter_map(|(session_id, session)| {
105                        trace!(?session_id, ?session);
106                        match &session.state {
107                            SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
108                                info!(%session_id, "Removing expired auth session");
109                                Some(PartialValue::Refer(*session_id))
110                            }
111                            _ => None,
112                        }
113                    })
114                    .collect()
115                });
116
117            if let Some(expired) = expired.as_ref() {
118                entry.remove_avas(Attribute::UserAuthTokenSession, expired);
119            }
120
121            let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map(Attribute::OAuth2Session).map(|oauth2_sessions| {
124                let sessions = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession);
126
127                oauth2_sessions.iter().filter_map(|(o2_session_id, session)| {
128                    trace!(?o2_session_id, ?session);
129                    match &session.state {
130                        SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
131                            info!(%o2_session_id, "Removing expired oauth2 session");
132                            Some(PartialValue::Refer(*o2_session_id))
133                        }
134                        SessionState::RevokedAt(_) => {
135                            trace!("Skip already revoked session");
137                            None
138                        }
139                        _ => {
140                            if sessions.map(|session_map| {
142                                    if let Some(parent_session_id) = session.parent.as_ref() {
143                                        if let Some(parent_session) = session_map.get(parent_session_id) {
145                                            !matches!(parent_session.state, SessionState::RevokedAt(_))
147                                        } else {
148                                            false
150                                        }
151                                    } else {
152                                        true
155                                    }
156                                }).unwrap_or(false) {
157                                    debug!("Parent session remains valid.");
159                                    None
160                                } else {
161                                    if session.issued_at + AUTH_TOKEN_GRACE_WINDOW <= curtime_odt {
163                                        info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
164                                        Some(PartialValue::Refer(*o2_session_id))
165                                    } else {
166                                        debug!("Not enforcing parent session consistency on session within grace window");
168                                        None
169                                    }
170
171                                }
172                        }
173                    }
174
175                })
176                .collect()
177            });
178
179            if let Some(oauth2_remove) = oauth2_remove.as_ref() {
180                entry.remove_avas(Attribute::OAuth2Session, oauth2_remove);
181            }
182
183            Ok(())
184        })
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use crate::prelude::*;
191
192    use crate::event::CreateEvent;
193    use crate::value::{AuthType, Oauth2Session, Session, SessionState};
194    use kanidm_proto::constants::OAUTH2_SCOPE_OPENID;
195    use std::time::Duration;
196    use time::OffsetDateTime;
197    use uuid::uuid;
198
199    use crate::credential::Credential;
200    use kanidm_lib_crypto::CryptoPolicy;
201
202    #[qs_test]
205    async fn test_session_consistency_expire_old_sessions(server: &QueryServer) {
206        let curtime = duration_from_epoch_now();
207        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
208
209        let p = CryptoPolicy::minimum();
210        let cred = Credential::new_password_only(&p, "test_password").unwrap();
211        let cred_id = cred.uuid;
212
213        let exp_curtime = curtime + Duration::from_secs(60);
214        let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
215
216        let mut server_txn = server.write(curtime).await.unwrap();
218
219        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
220
221        let e1 = entry_init!(
222            (Attribute::Class, EntryClass::Object.to_value()),
223            (Attribute::Class, EntryClass::Person.to_value()),
224            (Attribute::Class, EntryClass::Account.to_value()),
225            (Attribute::Name, Value::new_iname("testperson1")),
226            (Attribute::Uuid, Value::Uuid(tuuid)),
227            (Attribute::Description, Value::new_utf8s("testperson1")),
228            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
229            (
230                Attribute::PrimaryCredential,
231                Value::Cred("primary".to_string(), cred.clone())
232            )
233        );
234
235        let ce = CreateEvent::new_internal(vec![e1]);
236        assert!(server_txn.create(&ce).is_ok());
237
238        let session_id = Uuid::new_v4();
240        let state = SessionState::ExpiresAt(exp_curtime_odt);
241        let issued_at = curtime_odt;
242        let issued_by = IdentityId::User(tuuid);
243        let scope = SessionScope::ReadOnly;
244
245        let session = Value::Session(
246            session_id,
247            Session {
248                label: "label".to_string(),
249                state,
250                issued_at,
253                issued_by,
255                cred_id,
256                scope,
259                type_: AuthType::Passkey,
260            },
261        );
262
263        let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
265
266        server_txn
267            .internal_modify(
268                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
269                &modlist,
270            )
271            .expect("Failed to modify user");
272
273        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
276
277        let session = entry
278            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
279            .and_then(|sessions| sessions.get(&session_id))
280            .expect("No session map found");
281        assert!(matches!(session.state, SessionState::ExpiresAt(_)));
282
283        assert!(server_txn.commit().is_ok());
284        let mut server_txn = server.write(exp_curtime).await.unwrap();
285
286        let modlist = ModifyList::new_purge_and_set(
288            Attribute::Description,
289            Value::new_utf8s("test person 1 change"),
290        );
291
292        server_txn
293            .internal_modify(
294                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
295                &modlist,
296            )
297            .expect("Failed to modify user");
298
299        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
301
302        let session = entry
304            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
305            .and_then(|sessions| sessions.get(&session_id))
306            .expect("No session map found");
307        assert!(matches!(session.state, SessionState::RevokedAt(_)));
308
309        assert!(server_txn.commit().is_ok());
310    }
311
312    #[qs_test]
314    async fn test_session_consistency_oauth2_expiry_cleanup(server: &QueryServer) {
315        let curtime = duration_from_epoch_now();
316        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
317
318        let p = CryptoPolicy::minimum();
319        let cred = Credential::new_password_only(&p, "test_password").unwrap();
320        let cred_id = cred.uuid;
321
322        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
324        let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
325
326        let mut server_txn = server.write(curtime).await.unwrap();
328
329        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
330        let rs_uuid = Uuid::new_v4();
331
332        let e1 = entry_init!(
333            (Attribute::Class, EntryClass::Object.to_value()),
334            (Attribute::Class, EntryClass::Person.to_value()),
335            (Attribute::Class, EntryClass::Account.to_value()),
336            (Attribute::Name, Value::new_iname("testperson1")),
337            (Attribute::Uuid, Value::Uuid(tuuid)),
338            (Attribute::Description, Value::new_utf8s("testperson1")),
339            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
340            (
341                Attribute::PrimaryCredential,
342                Value::Cred("primary".to_string(), cred.clone())
343            )
344        );
345
346        let e2 = entry_init!(
347            (Attribute::Class, EntryClass::Object.to_value()),
348            (Attribute::Class, EntryClass::Account.to_value()),
349            (
350                Attribute::Class,
351                EntryClass::OAuth2ResourceServer.to_value()
352            ),
353            (
354                Attribute::Class,
355                EntryClass::OAuth2ResourceServerBasic.to_value()
356            ),
357            (Attribute::Uuid, Value::Uuid(rs_uuid)),
358            (Attribute::Name, Value::new_iname("test_resource_server")),
359            (
360                Attribute::DisplayName,
361                Value::new_utf8s("test_resource_server")
362            ),
363            (
364                Attribute::OAuth2RsOriginLanding,
365                Value::new_url_s("https://demo.example.com").unwrap()
366            ),
367            (
369                Attribute::OAuth2RsScopeMap,
370                Value::new_oauthscopemap(
371                    UUID_IDM_ALL_ACCOUNTS,
372                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
373                )
374                .expect("invalid oauthscope")
375            )
376        );
377
378        let ce = CreateEvent::new_internal(vec![e1, e2]);
379        assert!(server_txn.create(&ce).is_ok());
380
381        let session_id = Uuid::new_v4();
384        let parent_id = Uuid::new_v4();
385        let state = SessionState::ExpiresAt(exp_curtime_odt);
386        let issued_at = curtime_odt;
387        let issued_by = IdentityId::User(tuuid);
388        let scope = SessionScope::ReadOnly;
389
390        let modlist = modlist!([
392            Modify::Present(
393                "oauth2_session".into(),
394                Value::Oauth2Session(
395                    session_id,
396                    Oauth2Session {
397                        parent: Some(parent_id),
398                        state,
400                        issued_at,
401                        rs_uuid,
402                    },
403                )
404            ),
405            Modify::Present(
406                Attribute::UserAuthTokenSession,
407                Value::Session(
408                    parent_id,
409                    Session {
410                        label: "label".to_string(),
411                        state: SessionState::NeverExpires,
413                        issued_at,
416                        issued_by,
418                        cred_id,
419                        scope,
422                        type_: AuthType::Passkey,
423                    },
424                )
425            ),
426        ]);
427
428        server_txn
429            .internal_modify(
430                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
431                &modlist,
432            )
433            .expect("Failed to modify user");
434
435        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
438
439        let session = entry
440            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
441            .and_then(|sessions| sessions.get(&parent_id))
442            .expect("No session map found");
443        assert!(matches!(session.state, SessionState::NeverExpires));
444
445        let session = entry
446            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
447            .and_then(|sessions| sessions.get(&session_id))
448            .expect("No session map found");
449        assert!(matches!(session.state, SessionState::ExpiresAt(_)));
450
451        assert!(server_txn.commit().is_ok());
452
453        let mut server_txn = server.write(exp_curtime).await.unwrap();
456
457        let modlist = ModifyList::new_purge_and_set(
459            Attribute::Description,
460            Value::new_utf8s("test person 1 change"),
461        );
462
463        server_txn
464            .internal_modify(
465                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
466                &modlist,
467            )
468            .expect("Failed to modify user");
469
470        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
472
473        let session = entry
475            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
476            .and_then(|sessions| sessions.get(&parent_id))
477            .expect("No session map found");
478        assert!(matches!(session.state, SessionState::NeverExpires));
479
480        let session = entry
481            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
482            .and_then(|sessions| sessions.get(&session_id))
483            .expect("No session map found");
484        assert!(matches!(session.state, SessionState::RevokedAt(_)));
485
486        assert!(server_txn.commit().is_ok());
487    }
488
489    #[qs_test]
491    async fn test_session_consistency_oauth2_removed_by_parent(server: &QueryServer) {
492        let curtime = duration_from_epoch_now();
493        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
494        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
495
496        let p = CryptoPolicy::minimum();
497        let cred = Credential::new_password_only(&p, "test_password").unwrap();
498        let cred_id = cred.uuid;
499
500        let mut server_txn = server.write(curtime).await.unwrap();
502
503        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
504        let rs_uuid = Uuid::new_v4();
505
506        let e1 = entry_init!(
507            (Attribute::Class, EntryClass::Object.to_value()),
508            (Attribute::Class, EntryClass::Person.to_value()),
509            (Attribute::Class, EntryClass::Account.to_value()),
510            (Attribute::Name, Value::new_iname("testperson1")),
511            (Attribute::Uuid, Value::Uuid(tuuid)),
512            (Attribute::Description, Value::new_utf8s("testperson1")),
513            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
514            (
515                Attribute::PrimaryCredential,
516                Value::Cred("primary".to_string(), cred.clone())
517            )
518        );
519
520        let e2 = entry_init!(
521            (Attribute::Class, EntryClass::Object.to_value()),
522            (Attribute::Class, EntryClass::Account.to_value()),
523            (
524                Attribute::Class,
525                EntryClass::OAuth2ResourceServer.to_value()
526            ),
527            (
528                Attribute::Class,
529                EntryClass::OAuth2ResourceServerBasic.to_value()
530            ),
531            (Attribute::Uuid, Value::Uuid(rs_uuid)),
532            (Attribute::Name, Value::new_iname("test_resource_server")),
533            (
534                Attribute::DisplayName,
535                Value::new_utf8s("test_resource_server")
536            ),
537            (
538                Attribute::OAuth2RsOriginLanding,
539                Value::new_url_s("https://demo.example.com").unwrap()
540            ),
541            (
543                Attribute::OAuth2RsScopeMap,
544                Value::new_oauthscopemap(
545                    UUID_IDM_ALL_ACCOUNTS,
546                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
547                )
548                .expect("invalid oauthscope")
549            )
550        );
551
552        let ce = CreateEvent::new_internal(vec![e1, e2]);
553        assert!(server_txn.create(&ce).is_ok());
554
555        let session_id = Uuid::new_v4();
558        let parent_id = Uuid::new_v4();
559        let issued_at = curtime_odt;
560        let issued_by = IdentityId::User(tuuid);
561        let scope = SessionScope::ReadOnly;
562
563        let modlist = modlist!([
565            Modify::Present(
566                "oauth2_session".into(),
567                Value::Oauth2Session(
568                    session_id,
569                    Oauth2Session {
570                        parent: Some(parent_id),
571                        state: SessionState::NeverExpires,
573                        issued_at,
574                        rs_uuid,
575                    },
576                )
577            ),
578            Modify::Present(
579                Attribute::UserAuthTokenSession,
580                Value::Session(
581                    parent_id,
582                    Session {
583                        label: "label".to_string(),
584                        state: SessionState::NeverExpires,
586                        issued_at,
589                        issued_by,
591                        cred_id,
592                        scope,
595                        type_: AuthType::Passkey,
596                    },
597                )
598            ),
599        ]);
600
601        server_txn
602            .internal_modify(
603                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
604                &modlist,
605            )
606            .expect("Failed to modify user");
607
608        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
611
612        let session = entry
613            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
614            .and_then(|sessions| sessions.get(&parent_id))
615            .expect("No session map found");
616        assert!(matches!(session.state, SessionState::NeverExpires));
617
618        let session = entry
619            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
620            .and_then(|sessions| sessions.get(&session_id))
621            .expect("No session map found");
622        assert!(matches!(session.state, SessionState::NeverExpires));
623
624        assert!(server_txn.commit().is_ok());
626        let mut server_txn = server.write(exp_curtime).await.unwrap();
627
628        let modlist = ModifyList::new_remove(
630            Attribute::UserAuthTokenSession,
631            PartialValue::Refer(parent_id),
632        );
633
634        server_txn
635            .internal_modify(
636                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
637                &modlist,
638            )
639            .expect("Failed to modify user");
640
641        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
643
644        let session = entry
646            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
647            .and_then(|sessions| sessions.get(&parent_id))
648            .expect("No session map found");
649        assert!(matches!(session.state, SessionState::RevokedAt(_)));
650
651        let session = entry
653            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
654            .and_then(|sessions| sessions.get(&session_id))
655            .expect("No session map found");
656        assert!(matches!(session.state, SessionState::RevokedAt(_)));
657
658        assert!(server_txn.commit().is_ok());
659    }
660
661    #[qs_test]
663    async fn test_session_consistency_oauth2_grace_window_past(server: &QueryServer) {
664        let curtime = duration_from_epoch_now();
665        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
666
667        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
669        let mut server_txn = server.write(curtime).await.unwrap();
673
674        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
675        let rs_uuid = Uuid::new_v4();
676
677        let e1 = entry_init!(
678            (Attribute::Class, EntryClass::Object.to_value()),
679            (Attribute::Class, EntryClass::Person.to_value()),
680            (Attribute::Class, EntryClass::Account.to_value()),
681            (Attribute::Name, Value::new_iname("testperson1")),
682            (Attribute::Uuid, Value::Uuid(tuuid)),
683            (Attribute::Description, Value::new_utf8s("testperson1")),
684            (Attribute::DisplayName, Value::new_utf8s("testperson1"))
685        );
686
687        let e2 = entry_init!(
688            (Attribute::Class, EntryClass::Object.to_value()),
689            (Attribute::Class, EntryClass::Account.to_value()),
690            (
691                Attribute::Class,
692                EntryClass::OAuth2ResourceServer.to_value()
693            ),
694            (
695                Attribute::Class,
696                EntryClass::OAuth2ResourceServerBasic.to_value()
697            ),
698            (Attribute::Uuid, Value::Uuid(rs_uuid)),
699            (Attribute::Name, Value::new_iname("test_resource_server")),
700            (
701                Attribute::DisplayName,
702                Value::new_utf8s("test_resource_server")
703            ),
704            (
705                Attribute::OAuth2RsOriginLanding,
706                Value::new_url_s("https://demo.example.com").unwrap()
707            ),
708            (
710                Attribute::OAuth2RsScopeMap,
711                Value::new_oauthscopemap(
712                    UUID_IDM_ALL_ACCOUNTS,
713                    btreeset![OAUTH2_SCOPE_OPENID.to_string()]
714                )
715                .expect("invalid oauthscope")
716            )
717        );
718
719        let ce = CreateEvent::new_internal(vec![e1, e2]);
720        assert!(server_txn.create(&ce).is_ok());
721
722        let session_id = Uuid::new_v4();
724        let parent = Uuid::new_v4();
725        let issued_at = curtime_odt;
726
727        let session = Value::Oauth2Session(
728            session_id,
729            Oauth2Session {
730                parent: Some(parent),
731                state: SessionState::NeverExpires,
734                issued_at,
735                rs_uuid,
736            },
737        );
738
739        let modlist = ModifyList::new_append(Attribute::OAuth2Session, session);
741
742        server_txn
743            .internal_modify(
744                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
745                &modlist,
746            )
747            .expect("Failed to modify user");
748
749        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
752
753        let session = entry
754            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
755            .and_then(|sessions| sessions.get(&session_id))
756            .expect("No session map found");
757        assert!(matches!(session.state, SessionState::NeverExpires));
758
759        assert!(server_txn.commit().is_ok());
760
761        let mut server_txn = server.write(exp_curtime).await.unwrap();
764
765        let modlist = ModifyList::new_purge_and_set(
767            Attribute::Description,
768            Value::new_utf8s("test person 1 change"),
769        );
770
771        server_txn
772            .internal_modify(
773                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
774                &modlist,
775            )
776            .expect("Failed to modify user");
777
778        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
780
781        let session = entry
783            .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
784            .and_then(|sessions| sessions.get(&session_id))
785            .expect("No session map found");
786        assert!(matches!(session.state, SessionState::RevokedAt(_)));
787
788        assert!(server_txn.commit().is_ok());
789    }
790
791    #[qs_test]
792    async fn test_session_consistency_expire_when_cred_removed(server: &QueryServer) {
793        let curtime = duration_from_epoch_now();
794        let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
795
796        let p = CryptoPolicy::minimum();
797        let cred = Credential::new_password_only(&p, "test_password").unwrap();
798        let cred_id = cred.uuid;
799
800        let mut server_txn = server.write(curtime).await.unwrap();
802
803        let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
804
805        let e1 = entry_init!(
806            (Attribute::Class, EntryClass::Object.to_value()),
807            (Attribute::Class, EntryClass::Person.to_value()),
808            (Attribute::Class, EntryClass::Account.to_value()),
809            (Attribute::Name, Value::new_iname("testperson1")),
810            (Attribute::Uuid, Value::Uuid(tuuid)),
811            (Attribute::Description, Value::new_utf8s("testperson1")),
812            (Attribute::DisplayName, Value::new_utf8s("testperson1")),
813            (
814                Attribute::PrimaryCredential,
815                Value::Cred("primary".to_string(), cred.clone())
816            )
817        );
818
819        let ce = CreateEvent::new_internal(vec![e1]);
820        assert!(server_txn.create(&ce).is_ok());
821
822        let session_id = Uuid::new_v4();
824        let issued_at = curtime_odt;
826        let issued_by = IdentityId::User(tuuid);
827        let scope = SessionScope::ReadOnly;
828
829        let session = Value::Session(
830            session_id,
831            Session {
832                label: "label".to_string(),
833                state: SessionState::NeverExpires,
834                issued_at,
837                issued_by,
839                cred_id,
840                scope,
843                type_: AuthType::Passkey,
844            },
845        );
846
847        let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
849
850        server_txn
851            .internal_modify(
852                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
853                &modlist,
854            )
855            .expect("Failed to modify user");
856
857        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
860
861        let session = entry
862            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
863            .and_then(|sessions| sessions.get(&session_id))
864            .expect("No session map found");
865        assert!(matches!(session.state, SessionState::NeverExpires));
866
867        assert!(server_txn.commit().is_ok());
868
869        let mut server_txn = server.write(curtime).await.unwrap();
871
872        let modlist = ModifyList::new_purge(Attribute::PrimaryCredential);
874
875        server_txn
876            .internal_modify(
877                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
878                &modlist,
879            )
880            .expect("Failed to modify user");
881
882        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
884
885        let session = entry
887            .get_ava_as_session_map(Attribute::UserAuthTokenSession)
888            .and_then(|sessions| sessions.get(&session_id))
889            .expect("No session map found");
890        assert!(matches!(session.state, SessionState::RevokedAt(_)));
891
892        assert!(server_txn.commit().is_ok());
893    }
894}