kanidmd_lib/plugins/
session.rs

1//! This plugin maintains consistency of authenticated sessions on accounts.
2//!
3//! An example of this is that oauth2 sessions are child of user auth sessions,
4//! such than when the user auth session is terminated, then the corresponding
5//! oauth2 session should also be terminated.
6//!
7//! This plugin is also responsible for invaliding old sessions that are past
8//! their expiry.
9
10use 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        // We need to assert a number of properties. We must do these *in order*.
56        cand.iter_mut().try_for_each(|entry| {
57            // * If the session's credential is no longer on the account, we remove the session.
58            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                                // Ignore, it's already revoked.
82                                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            // * If a UAT is past its expiry, remove it.
102            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            // * If an oauth2 session is past it's expiry, remove it.
122            // * If an oauth2 session is past the grace window, and no parent session exists, remove it.
123            let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map(Attribute::OAuth2Session).map(|oauth2_sessions| {
124                // If we have oauth2 sessions, we need to be able to lookup if sessions exist in the uat.
125                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                            // no-op, it's already revoked.
136                            trace!("Skip already revoked session");
137                            None
138                        }
139                        _ => {
140                            // Okay, now check the issued / grace time for parent enforcement.
141                                if sessions.map(|session_map| {
142                                    if let Some(parent_session_id) = session.parent.as_ref() {
143                                        // A parent session id exists - validate it exists in the account.
144                                        if let Some(parent_session) = session_map.get(parent_session_id) {
145                                            // Only match non-revoked sessions
146                                            !matches!(parent_session.state, SessionState::RevokedAt(_))
147                                        } else {
148                                            // not found
149                                            false
150                                        }
151                                    } else {
152                                        // The session specifically has no parent session and so is
153                                        // not bounded by it's presence.
154                                        true
155                                    }
156                                }).unwrap_or(false) {
157                                    // The parent exists and is still valid, go ahead
158                                    debug!("Parent session remains valid.");
159                                    None
160                                } else {
161                                    // Can't find the parent. Are we within grace window
162                                    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                                        // Grace window is still in effect
167                                        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    // Test expiry of old sessions
203
204    #[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        // Create a user
217        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        // Create a fake session.
239        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                // Need the other inner bits?
251                // for the gracewindow.
252                issued_at,
253                // Who actually created this?
254                issued_by,
255                cred_id,
256                // What is the access scope of this session? This is
257                // for auditing purposes.
258                scope,
259                type_: AuthType::Passkey,
260            },
261        );
262
263        // Mod the user
264        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        // Still there
274
275        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        // Mod again - anything will do.
287        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        // Session gone.
300        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
301
302        // We get the attribute and have to check it's now in a revoked state.
303        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    // Test expiry of old oauth2 sessions
313    #[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        // Set exp to gracewindow.
323        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
324        let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
325
326        // Create a user
327        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            // System admins
368            (
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        // Create a fake session and oauth2 session.
382
383        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        // Mod the user
391        let modlist = modlist!([
392            Modify::Present(
393                "oauth2_session".into(),
394                Value::Oauth2Session(
395                    session_id,
396                    Oauth2Session {
397                        parent: Some(parent_id),
398                        // Set to the exp window.
399                        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                        // Note we set the exp to None so we are not removing based on removal of the parent.
412                        state: SessionState::NeverExpires,
413                        // Need the other inner bits?
414                        // for the gracewindow.
415                        issued_at,
416                        // Who actually created this?
417                        issued_by,
418                        cred_id,
419                        // What is the access scope of this session? This is
420                        // for auditing purposes.
421                        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        // Still there
436
437        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        // Note as we are now past exp time, the oauth2 session will be removed, but the uat session
454        // will remain.
455        let mut server_txn = server.write(exp_curtime).await.unwrap();
456
457        // Mod again - anything will do.
458        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        // Session gone.
471        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
472
473        // Note the uat is still present
474        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    // test removal of a session removes related oauth2 sessions.
490    #[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        // Create a user
501        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            // System admins
542            (
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        // Create a fake session and oauth2 session.
556
557        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        // Mod the user
564        let modlist = modlist!([
565            Modify::Present(
566                "oauth2_session".into(),
567                Value::Oauth2Session(
568                    session_id,
569                    Oauth2Session {
570                        parent: Some(parent_id),
571                        // Note we set the exp to None so we are not removing based on exp
572                        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                        // Note we set the exp to None so we are not removing based on removal of the parent.
585                        state: SessionState::NeverExpires,
586                        // Need the other inner bits?
587                        // for the gracewindow.
588                        issued_at,
589                        // Who actually created this?
590                        issued_by,
591                        cred_id,
592                        // What is the access scope of this session? This is
593                        // for auditing purposes.
594                        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        // Still there
609
610        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        // We need the time to be past grace_window.
625        assert!(server_txn.commit().is_ok());
626        let mut server_txn = server.write(exp_curtime).await.unwrap();
627
628        // Mod again - remove the parent session.
629        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        // Session gone.
642        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
643
644        // Note the uat is removed
645        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        // The oauth2 session is also removed.
652        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    // Test if an oauth2 session exists, the grace window passes and it's UAT doesn't exist.
662    #[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        // Set exp to gracewindow.
668        let exp_curtime = curtime + AUTH_TOKEN_GRACE_WINDOW;
669        // let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
670
671        // Create a user
672        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            // System admins
709            (
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        // Create a fake session.
723        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                // Note we set the exp to None so we are asserting the removal is due to the lack
732                // of the parent session.
733                state: SessionState::NeverExpires,
734                issued_at,
735                rs_uuid,
736            },
737        );
738
739        // Mod the user
740        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        // Still there
750
751        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        // Note the exp_curtime now is past the gracewindow. This will trigger
762        // consistency to purge the un-matched session.
763        let mut server_txn = server.write(exp_curtime).await.unwrap();
764
765        // Mod again - anything will do.
766        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        // Session gone.
779        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
780
781        // Note it's a not condition now.
782        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        // Create a user
801        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        // Create a fake session.
823        let session_id = Uuid::new_v4();
824        // No expiry!
825        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                // Need the other inner bits?
835                // for the gracewindow.
836                issued_at,
837                // Who actually created this?
838                issued_by,
839                cred_id,
840                // What is the access scope of this session? This is
841                // for auditing purposes.
842                scope,
843                type_: AuthType::Passkey,
844            },
845        );
846
847        // Mod the user
848        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        // Still there
858
859        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        // Notice we keep the time the same for the txn.
870        let mut server_txn = server.write(curtime).await.unwrap();
871
872        // Remove the primary credential
873        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        // Session gone.
883        let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
884
885        // Note it's a not condition now.
886        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}