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 .chain(
75 entry.get_ava_single_uuid(Attribute::OAuth2AccountCredentialUuid)
76 )
77 .collect();
78
79 let invalidate: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
80 .map(|sessions| {
81 sessions.iter().filter_map(|(session_id, session)| {
82 match &session.state {
83 SessionState::RevokedAt(_) => {
84 None
86 }
87 SessionState::ExpiresAt(_) |
88 SessionState::NeverExpires =>
89 if !cred_ids.contains(&session.cred_id) {
90 info!(%session_id, "Revoking auth session whose issuing credential no longer exists");
91 Some(PartialValue::Refer(*session_id))
92 } else {
93 None
94 },
95 }
96 })
97 .collect()
98 });
99
100 if let Some(invalidate) = invalidate.as_ref() {
101 entry.remove_avas(Attribute::UserAuthTokenSession, invalidate);
102 }
103
104 let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
106 .map(|sessions| {
107 sessions.iter().filter_map(|(session_id, session)| {
108 trace!(?session_id, ?session);
109 match &session.state {
110 SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
111 info!(%session_id, "Removing expired auth session");
112 Some(PartialValue::Refer(*session_id))
113 }
114 _ => None,
115 }
116 })
117 .collect()
118 });
119
120 if let Some(expired) = expired.as_ref() {
121 entry.remove_avas(Attribute::UserAuthTokenSession, expired);
122 }
123
124 let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map(Attribute::OAuth2Session).map(|oauth2_sessions| {
127 let sessions = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession);
129
130 oauth2_sessions.iter().filter_map(|(o2_session_id, session)| {
131 trace!(?o2_session_id, ?session);
132 match &session.state {
133 SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
134 info!(%o2_session_id, "Removing expired oauth2 session");
135 Some(PartialValue::Refer(*o2_session_id))
136 }
137 SessionState::RevokedAt(_) => {
138 trace!("Skip already revoked session");
140 None
141 }
142 _ => {
143 if sessions.map(|session_map| {
145 if let Some(parent_session_id) = session.parent.as_ref() {
146 if let Some(parent_session) = session_map.get(parent_session_id) {
148 !matches!(parent_session.state, SessionState::RevokedAt(_))
150 } else {
151 false
153 }
154 } else {
155 true
158 }
159 }).unwrap_or(false) {
160 debug!("Parent session remains valid.");
162 None
163 } else {
164 if session.issued_at + AUTH_TOKEN_GRACE_WINDOW <= curtime_odt {
166 info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
167 Some(PartialValue::Refer(*o2_session_id))
168 } else {
169 debug!("Not enforcing parent session consistency on session within grace window");
171 None
172 }
173
174 }
175 }
176 }
177
178 })
179 .collect()
180 });
181
182 if let Some(oauth2_remove) = oauth2_remove.as_ref() {
183 entry.remove_avas(Attribute::OAuth2Session, oauth2_remove);
184 }
185
186 Ok(())
187 })
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use crate::prelude::*;
194
195 use crate::event::CreateEvent;
196 use crate::value::{AuthType, Oauth2Session, Session, SessionState};
197 use kanidm_proto::constants::OAUTH2_SCOPE_OPENID;
198 use std::time::Duration;
199 use time::OffsetDateTime;
200 use uuid::uuid;
201
202 use crate::credential::Credential;
203 use kanidm_lib_crypto::CryptoPolicy;
204
205 #[qs_test]
208 async fn test_session_consistency_expire_old_sessions(server: &QueryServer) {
209 let curtime = duration_from_epoch_now();
210 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
211
212 let p = CryptoPolicy::minimum();
213 let cred = Credential::new_password_only(&p, "test_password").unwrap();
214 let cred_id = cred.uuid;
215
216 let exp_curtime = curtime + Duration::from_secs(60);
217 let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
218
219 let mut server_txn = server.write(curtime).await.unwrap();
221
222 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
223
224 let e1 = entry_init!(
225 (Attribute::Class, EntryClass::Object.to_value()),
226 (Attribute::Class, EntryClass::Person.to_value()),
227 (Attribute::Class, EntryClass::Account.to_value()),
228 (Attribute::Name, Value::new_iname("testperson1")),
229 (Attribute::Uuid, Value::Uuid(tuuid)),
230 (Attribute::Description, Value::new_utf8s("testperson1")),
231 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
232 (
233 Attribute::PrimaryCredential,
234 Value::Cred("primary".to_string(), cred.clone())
235 )
236 );
237
238 let ce = CreateEvent::new_internal(vec![e1]);
239 assert!(server_txn.create(&ce).is_ok());
240
241 let session_id = Uuid::new_v4();
243 let state = SessionState::ExpiresAt(exp_curtime_odt);
244 let issued_at = curtime_odt;
245 let issued_by = IdentityId::User(tuuid);
246 let scope = SessionScope::ReadOnly;
247
248 let session = Value::Session(
249 session_id,
250 Session {
251 label: "label".to_string(),
252 state,
253 issued_at,
256 issued_by,
258 cred_id,
259 scope,
262 type_: AuthType::Passkey,
263 ext_metadata: Default::default(),
264 },
265 );
266
267 let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
269
270 server_txn
271 .internal_modify(
272 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
273 &modlist,
274 )
275 .expect("Failed to modify user");
276
277 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
280
281 let session = entry
282 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
283 .and_then(|sessions| sessions.get(&session_id))
284 .expect("No session map found");
285 assert!(matches!(session.state, SessionState::ExpiresAt(_)));
286
287 assert!(server_txn.commit().is_ok());
288 let mut server_txn = server.write(exp_curtime).await.unwrap();
289
290 let modlist = ModifyList::new_purge_and_set(
292 Attribute::Description,
293 Value::new_utf8s("test person 1 change"),
294 );
295
296 server_txn
297 .internal_modify(
298 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
299 &modlist,
300 )
301 .expect("Failed to modify user");
302
303 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
305
306 let session = entry
308 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
309 .and_then(|sessions| sessions.get(&session_id))
310 .expect("No session map found");
311 assert!(matches!(session.state, SessionState::RevokedAt(_)));
312
313 assert!(server_txn.commit().is_ok());
314 }
315
316 #[qs_test]
318 async fn test_session_consistency_oauth2_expiry_cleanup(server: &QueryServer) {
319 let curtime = duration_from_epoch_now();
320 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
321
322 let p = CryptoPolicy::minimum();
323 let cred = Credential::new_password_only(&p, "test_password").unwrap();
324 let cred_id = cred.uuid;
325
326 let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
328 let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
329
330 let mut server_txn = server.write(curtime).await.unwrap();
332
333 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
334 let rs_uuid = Uuid::new_v4();
335
336 let e1 = entry_init!(
337 (Attribute::Class, EntryClass::Object.to_value()),
338 (Attribute::Class, EntryClass::Person.to_value()),
339 (Attribute::Class, EntryClass::Account.to_value()),
340 (Attribute::Name, Value::new_iname("testperson1")),
341 (Attribute::Uuid, Value::Uuid(tuuid)),
342 (Attribute::Description, Value::new_utf8s("testperson1")),
343 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
344 (
345 Attribute::PrimaryCredential,
346 Value::Cred("primary".to_string(), cred.clone())
347 )
348 );
349
350 let e2 = entry_init!(
351 (Attribute::Class, EntryClass::Object.to_value()),
352 (Attribute::Class, EntryClass::Account.to_value()),
353 (
354 Attribute::Class,
355 EntryClass::OAuth2ResourceServer.to_value()
356 ),
357 (
358 Attribute::Class,
359 EntryClass::OAuth2ResourceServerBasic.to_value()
360 ),
361 (Attribute::Uuid, Value::Uuid(rs_uuid)),
362 (Attribute::Name, Value::new_iname("test_resource_server")),
363 (
364 Attribute::DisplayName,
365 Value::new_utf8s("test_resource_server")
366 ),
367 (
368 Attribute::OAuth2RsOriginLanding,
369 Value::new_url_s("https://demo.example.com").unwrap()
370 ),
371 (
373 Attribute::OAuth2RsScopeMap,
374 Value::new_oauthscopemap(
375 UUID_IDM_ALL_ACCOUNTS,
376 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
377 )
378 .expect("invalid oauthscope")
379 )
380 );
381
382 let ce = CreateEvent::new_internal(vec![e1, e2]);
383 assert!(server_txn.create(&ce).is_ok());
384
385 let session_id = Uuid::new_v4();
388 let parent_id = Uuid::new_v4();
389 let state = SessionState::ExpiresAt(exp_curtime_odt);
390 let issued_at = curtime_odt;
391 let issued_by = IdentityId::User(tuuid);
392 let scope = SessionScope::ReadOnly;
393
394 let modlist = modlist!([
396 Modify::Present(
397 "oauth2_session".into(),
398 Value::Oauth2Session(
399 session_id,
400 Oauth2Session {
401 parent: Some(parent_id),
402 state,
404 issued_at,
405 rs_uuid,
406 },
407 )
408 ),
409 Modify::Present(
410 Attribute::UserAuthTokenSession,
411 Value::Session(
412 parent_id,
413 Session {
414 label: "label".to_string(),
415 state: SessionState::NeverExpires,
417 issued_at,
420 issued_by,
422 cred_id,
423 scope,
426 type_: AuthType::Passkey,
427 ext_metadata: Default::default(),
428 },
429 )
430 ),
431 ]);
432
433 server_txn
434 .internal_modify(
435 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
436 &modlist,
437 )
438 .expect("Failed to modify user");
439
440 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
443
444 let session = entry
445 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
446 .and_then(|sessions| sessions.get(&parent_id))
447 .expect("No session map found");
448 assert!(matches!(session.state, SessionState::NeverExpires));
449
450 let session = entry
451 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
452 .and_then(|sessions| sessions.get(&session_id))
453 .expect("No session map found");
454 assert!(matches!(session.state, SessionState::ExpiresAt(_)));
455
456 assert!(server_txn.commit().is_ok());
457
458 let mut server_txn = server.write(exp_curtime).await.unwrap();
461
462 let modlist = ModifyList::new_purge_and_set(
464 Attribute::Description,
465 Value::new_utf8s("test person 1 change"),
466 );
467
468 server_txn
469 .internal_modify(
470 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
471 &modlist,
472 )
473 .expect("Failed to modify user");
474
475 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
477
478 let session = entry
480 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
481 .and_then(|sessions| sessions.get(&parent_id))
482 .expect("No session map found");
483 assert!(matches!(session.state, SessionState::NeverExpires));
484
485 let session = entry
486 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
487 .and_then(|sessions| sessions.get(&session_id))
488 .expect("No session map found");
489 assert!(matches!(session.state, SessionState::RevokedAt(_)));
490
491 assert!(server_txn.commit().is_ok());
492 }
493
494 #[qs_test]
496 async fn test_session_consistency_oauth2_removed_by_parent(server: &QueryServer) {
497 let curtime = duration_from_epoch_now();
498 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
499 let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
500
501 let p = CryptoPolicy::minimum();
502 let cred = Credential::new_password_only(&p, "test_password").unwrap();
503 let cred_id = cred.uuid;
504
505 let mut server_txn = server.write(curtime).await.unwrap();
507
508 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
509 let rs_uuid = Uuid::new_v4();
510
511 let e1 = entry_init!(
512 (Attribute::Class, EntryClass::Object.to_value()),
513 (Attribute::Class, EntryClass::Person.to_value()),
514 (Attribute::Class, EntryClass::Account.to_value()),
515 (Attribute::Name, Value::new_iname("testperson1")),
516 (Attribute::Uuid, Value::Uuid(tuuid)),
517 (Attribute::Description, Value::new_utf8s("testperson1")),
518 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
519 (
520 Attribute::PrimaryCredential,
521 Value::Cred("primary".to_string(), cred.clone())
522 )
523 );
524
525 let e2 = entry_init!(
526 (Attribute::Class, EntryClass::Object.to_value()),
527 (Attribute::Class, EntryClass::Account.to_value()),
528 (
529 Attribute::Class,
530 EntryClass::OAuth2ResourceServer.to_value()
531 ),
532 (
533 Attribute::Class,
534 EntryClass::OAuth2ResourceServerBasic.to_value()
535 ),
536 (Attribute::Uuid, Value::Uuid(rs_uuid)),
537 (Attribute::Name, Value::new_iname("test_resource_server")),
538 (
539 Attribute::DisplayName,
540 Value::new_utf8s("test_resource_server")
541 ),
542 (
543 Attribute::OAuth2RsOriginLanding,
544 Value::new_url_s("https://demo.example.com").unwrap()
545 ),
546 (
548 Attribute::OAuth2RsScopeMap,
549 Value::new_oauthscopemap(
550 UUID_IDM_ALL_ACCOUNTS,
551 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
552 )
553 .expect("invalid oauthscope")
554 )
555 );
556
557 let ce = CreateEvent::new_internal(vec![e1, e2]);
558 assert!(server_txn.create(&ce).is_ok());
559
560 let session_id = Uuid::new_v4();
563 let parent_id = Uuid::new_v4();
564 let issued_at = curtime_odt;
565 let issued_by = IdentityId::User(tuuid);
566 let scope = SessionScope::ReadOnly;
567
568 let modlist = modlist!([
570 Modify::Present(
571 "oauth2_session".into(),
572 Value::Oauth2Session(
573 session_id,
574 Oauth2Session {
575 parent: Some(parent_id),
576 state: SessionState::NeverExpires,
578 issued_at,
579 rs_uuid,
580 },
581 )
582 ),
583 Modify::Present(
584 Attribute::UserAuthTokenSession,
585 Value::Session(
586 parent_id,
587 Session {
588 label: "label".to_string(),
589 state: SessionState::NeverExpires,
591 issued_at,
594 issued_by,
596 cred_id,
597 scope,
600 type_: AuthType::Passkey,
601 ext_metadata: Default::default(),
602 },
603 )
604 ),
605 ]);
606
607 server_txn
608 .internal_modify(
609 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
610 &modlist,
611 )
612 .expect("Failed to modify user");
613
614 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
617
618 let session = entry
619 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
620 .and_then(|sessions| sessions.get(&parent_id))
621 .expect("No session map found");
622 assert!(matches!(session.state, SessionState::NeverExpires));
623
624 let session = entry
625 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
626 .and_then(|sessions| sessions.get(&session_id))
627 .expect("No session map found");
628 assert!(matches!(session.state, SessionState::NeverExpires));
629
630 assert!(server_txn.commit().is_ok());
632 let mut server_txn = server.write(exp_curtime).await.unwrap();
633
634 let modlist = ModifyList::new_remove(
636 Attribute::UserAuthTokenSession,
637 PartialValue::Refer(parent_id),
638 );
639
640 server_txn
641 .internal_modify(
642 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
643 &modlist,
644 )
645 .expect("Failed to modify user");
646
647 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
649
650 let session = entry
652 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
653 .and_then(|sessions| sessions.get(&parent_id))
654 .expect("No session map found");
655 assert!(matches!(session.state, SessionState::RevokedAt(_)));
656
657 let session = entry
659 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
660 .and_then(|sessions| sessions.get(&session_id))
661 .expect("No session map found");
662 assert!(matches!(session.state, SessionState::RevokedAt(_)));
663
664 assert!(server_txn.commit().is_ok());
665 }
666
667 #[qs_test]
669 async fn test_session_consistency_oauth2_grace_window_past(server: &QueryServer) {
670 let curtime = duration_from_epoch_now();
671 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
672
673 let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
675 let mut server_txn = server.write(curtime).await.unwrap();
679
680 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
681 let rs_uuid = Uuid::new_v4();
682
683 let e1 = entry_init!(
684 (Attribute::Class, EntryClass::Object.to_value()),
685 (Attribute::Class, EntryClass::Person.to_value()),
686 (Attribute::Class, EntryClass::Account.to_value()),
687 (Attribute::Name, Value::new_iname("testperson1")),
688 (Attribute::Uuid, Value::Uuid(tuuid)),
689 (Attribute::Description, Value::new_utf8s("testperson1")),
690 (Attribute::DisplayName, Value::new_utf8s("testperson1"))
691 );
692
693 let e2 = entry_init!(
694 (Attribute::Class, EntryClass::Object.to_value()),
695 (Attribute::Class, EntryClass::Account.to_value()),
696 (
697 Attribute::Class,
698 EntryClass::OAuth2ResourceServer.to_value()
699 ),
700 (
701 Attribute::Class,
702 EntryClass::OAuth2ResourceServerBasic.to_value()
703 ),
704 (Attribute::Uuid, Value::Uuid(rs_uuid)),
705 (Attribute::Name, Value::new_iname("test_resource_server")),
706 (
707 Attribute::DisplayName,
708 Value::new_utf8s("test_resource_server")
709 ),
710 (
711 Attribute::OAuth2RsOriginLanding,
712 Value::new_url_s("https://demo.example.com").unwrap()
713 ),
714 (
716 Attribute::OAuth2RsScopeMap,
717 Value::new_oauthscopemap(
718 UUID_IDM_ALL_ACCOUNTS,
719 btreeset![OAUTH2_SCOPE_OPENID.to_string()]
720 )
721 .expect("invalid oauthscope")
722 )
723 );
724
725 let ce = CreateEvent::new_internal(vec![e1, e2]);
726 assert!(server_txn.create(&ce).is_ok());
727
728 let session_id = Uuid::new_v4();
730 let parent = Uuid::new_v4();
731 let issued_at = curtime_odt;
732
733 let session = Value::Oauth2Session(
734 session_id,
735 Oauth2Session {
736 parent: Some(parent),
737 state: SessionState::NeverExpires,
740 issued_at,
741 rs_uuid,
742 },
743 );
744
745 let modlist = ModifyList::new_append(Attribute::OAuth2Session, session);
747
748 server_txn
749 .internal_modify(
750 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
751 &modlist,
752 )
753 .expect("Failed to modify user");
754
755 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
758
759 let session = entry
760 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
761 .and_then(|sessions| sessions.get(&session_id))
762 .expect("No session map found");
763 assert!(matches!(session.state, SessionState::NeverExpires));
764
765 assert!(server_txn.commit().is_ok());
766
767 let mut server_txn = server.write(exp_curtime).await.unwrap();
770
771 let modlist = ModifyList::new_purge_and_set(
773 Attribute::Description,
774 Value::new_utf8s("test person 1 change"),
775 );
776
777 server_txn
778 .internal_modify(
779 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
780 &modlist,
781 )
782 .expect("Failed to modify user");
783
784 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
786
787 let session = entry
789 .get_ava_as_oauth2session_map(Attribute::OAuth2Session)
790 .and_then(|sessions| sessions.get(&session_id))
791 .expect("No session map found");
792 assert!(matches!(session.state, SessionState::RevokedAt(_)));
793
794 assert!(server_txn.commit().is_ok());
795 }
796
797 #[qs_test]
798 async fn test_session_consistency_expire_when_cred_removed(server: &QueryServer) {
799 let curtime = duration_from_epoch_now();
800 let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
801
802 let p = CryptoPolicy::minimum();
803 let cred = Credential::new_password_only(&p, "test_password").unwrap();
804 let cred_id = cred.uuid;
805
806 let mut server_txn = server.write(curtime).await.unwrap();
808
809 let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
810
811 let e1 = entry_init!(
812 (Attribute::Class, EntryClass::Object.to_value()),
813 (Attribute::Class, EntryClass::Person.to_value()),
814 (Attribute::Class, EntryClass::Account.to_value()),
815 (Attribute::Name, Value::new_iname("testperson1")),
816 (Attribute::Uuid, Value::Uuid(tuuid)),
817 (Attribute::Description, Value::new_utf8s("testperson1")),
818 (Attribute::DisplayName, Value::new_utf8s("testperson1")),
819 (
820 Attribute::PrimaryCredential,
821 Value::Cred("primary".to_string(), cred.clone())
822 )
823 );
824
825 let ce = CreateEvent::new_internal(vec![e1]);
826 assert!(server_txn.create(&ce).is_ok());
827
828 let session_id = Uuid::new_v4();
830 let issued_at = curtime_odt;
832 let issued_by = IdentityId::User(tuuid);
833 let scope = SessionScope::ReadOnly;
834
835 let session = Value::Session(
836 session_id,
837 Session {
838 label: "label".to_string(),
839 state: SessionState::NeverExpires,
840 issued_at,
843 issued_by,
845 cred_id,
846 scope,
849 type_: AuthType::Passkey,
850 ext_metadata: Default::default(),
851 },
852 );
853
854 let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
856
857 server_txn
858 .internal_modify(
859 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
860 &modlist,
861 )
862 .expect("Failed to modify user");
863
864 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
867
868 let session = entry
869 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
870 .and_then(|sessions| sessions.get(&session_id))
871 .expect("No session map found");
872 assert!(matches!(session.state, SessionState::NeverExpires));
873
874 assert!(server_txn.commit().is_ok());
875
876 let mut server_txn = server.write(curtime).await.unwrap();
878
879 let modlist = ModifyList::new_purge(Attribute::PrimaryCredential);
881
882 server_txn
883 .internal_modify(
884 &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
885 &modlist,
886 )
887 .expect("Failed to modify user");
888
889 let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
891
892 let session = entry
894 .get_ava_as_session_map(Attribute::UserAuthTokenSession)
895 .and_then(|sessions| sessions.get(&session_id))
896 .expect("No session map found");
897 assert!(matches!(session.state, SessionState::RevokedAt(_)));
898
899 assert!(server_txn.commit().is_ok());
900 }
901}