kanidmd_lib/valueset/
session.rs

1use crate::be::dbvalue::{
2    DbCidV1, DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueAuthTypeV1,
3    DbValueIdentityId, DbValueOauth2Session, DbValueSession, DbValueSessionStateV1,
4};
5use crate::prelude::*;
6use crate::repl::cid::Cid;
7use crate::schema::SchemaAttribute;
8use crate::value::{
9    ApiToken, ApiTokenScope, AuthType, Oauth2Session, Session, SessionScope, SessionState,
10};
11use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ValueSet};
12use kanidm_proto::scim_v1::server::ScimApiToken;
13use kanidm_proto::scim_v1::server::ScimAuthSession;
14use kanidm_proto::scim_v1::server::ScimOAuth2Session;
15use std::collections::btree_map::Entry as BTreeEntry;
16use std::collections::BTreeMap;
17use time::OffsetDateTime;
18
19#[derive(Debug, Clone)]
20pub struct ValueSetSession {
21    map: BTreeMap<Uuid, Session>,
22}
23
24impl ValueSetSession {
25    pub fn new(u: Uuid, m: Session) -> Box<Self> {
26        let mut map = BTreeMap::new();
27        map.insert(u, m);
28        Box::new(ValueSetSession { map })
29    }
30
31    pub fn push(&mut self, u: Uuid, m: Session) -> bool {
32        self.map.insert(u, m).is_none()
33    }
34
35    fn to_vec_dbvs(&self) -> Vec<DbValueSession> {
36        self.map
37            .iter()
38            .map(|(u, m)| DbValueSession::V4 {
39                refer: *u,
40                label: m.label.clone(),
41
42                state: match &m.state {
43                    SessionState::ExpiresAt(odt) => {
44                        debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
45                        #[allow(clippy::expect_used)]
46                        odt.format(&Rfc3339)
47                            .map(DbValueSessionStateV1::ExpiresAt)
48                            .expect("Failed to format timestamp into RFC3339!")
49                    }
50                    SessionState::NeverExpires => DbValueSessionStateV1::Never,
51                    SessionState::RevokedAt(c) => DbValueSessionStateV1::RevokedAt(DbCidV1 {
52                        server_id: c.s_uuid,
53                        timestamp: c.ts,
54                    }),
55                },
56
57                issued_at: {
58                    debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
59                    #[allow(clippy::expect_used)]
60                    m.issued_at
61                        .format(&Rfc3339)
62                        .expect("Failed to format timestamp into RFC3339!")
63                },
64                issued_by: match m.issued_by {
65                    IdentityId::Internal => DbValueIdentityId::V1Internal,
66                    IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
67                    IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
68                },
69                cred_id: m.cred_id,
70                scope: match m.scope {
71                    SessionScope::ReadOnly => DbValueAccessScopeV1::ReadOnly,
72                    SessionScope::ReadWrite => DbValueAccessScopeV1::ReadWrite,
73                    SessionScope::PrivilegeCapable => DbValueAccessScopeV1::PrivilegeCapable,
74                    SessionScope::Synchronise => DbValueAccessScopeV1::Synchronise,
75                },
76                type_: match m.type_ {
77                    AuthType::Anonymous => DbValueAuthTypeV1::Anonymous,
78                    AuthType::Password => DbValueAuthTypeV1::Password,
79                    AuthType::GeneratedPassword => DbValueAuthTypeV1::GeneratedPassword,
80                    AuthType::PasswordTotp => DbValueAuthTypeV1::PasswordTotp,
81                    AuthType::PasswordBackupCode => DbValueAuthTypeV1::PasswordBackupCode,
82                    AuthType::PasswordSecurityKey => DbValueAuthTypeV1::PasswordSecurityKey,
83                    AuthType::Passkey => DbValueAuthTypeV1::Passkey,
84                    AuthType::AttestedPasskey => DbValueAuthTypeV1::AttestedPasskey,
85                },
86            })
87            .collect()
88    }
89
90    fn from_dbv_iter<'a>(
91        iter: impl Iterator<Item = &'a DbValueSession>,
92    ) -> Result<ValueSet, OperationError> {
93        let map = iter
94            .filter_map(|dbv| {
95                match dbv {
96                    // We need to ignore all older session records as they lack the AuthType
97                    // record which prevents re-auth working.
98                    DbValueSession::V1 { .. }
99                    | DbValueSession::V2 { .. }
100                    | DbValueSession::V3 { .. } => None,
101                    DbValueSession::V4 {
102                        refer,
103                        label,
104                        state,
105                        issued_at,
106                        issued_by,
107                        cred_id,
108                        scope,
109                        type_,
110                    } => {
111                        // Convert things.
112                        let issued_at = OffsetDateTime::parse(issued_at, &Rfc3339)
113                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
114                            .map_err(|e| {
115                                admin_error!(
116                                    ?e,
117                                    "Invalidating session {} due to invalid issued_at timestamp",
118                                    refer
119                                )
120                            })
121                            .ok()?;
122
123                        let state = match state {
124                            DbValueSessionStateV1::ExpiresAt(e_inner) => {
125                                OffsetDateTime::parse(e_inner, &Rfc3339)
126                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
127                                    .map(SessionState::ExpiresAt)
128                                    .map_err(|e| {
129                                        admin_error!(
130                                        ?e,
131                                        "Invalidating session {} due to invalid expiry timestamp",
132                                        refer
133                                    )
134                                    })
135                                    .ok()?
136                            }
137                            DbValueSessionStateV1::Never => SessionState::NeverExpires,
138                            DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
139                                s_uuid: dc.server_id,
140                                ts: dc.timestamp,
141                            }),
142                        };
143
144                        let issued_by = match issued_by {
145                            DbValueIdentityId::V1Internal => IdentityId::Internal,
146                            DbValueIdentityId::V1Uuid(u) => IdentityId::User(*u),
147                            DbValueIdentityId::V1Sync(u) => IdentityId::Synch(*u),
148                        };
149
150                        let scope = match scope {
151                            DbValueAccessScopeV1::IdentityOnly | DbValueAccessScopeV1::ReadOnly => {
152                                SessionScope::ReadOnly
153                            }
154                            DbValueAccessScopeV1::ReadWrite => SessionScope::ReadWrite,
155                            DbValueAccessScopeV1::PrivilegeCapable => {
156                                SessionScope::PrivilegeCapable
157                            }
158                            DbValueAccessScopeV1::Synchronise => SessionScope::Synchronise,
159                        };
160
161                        let type_ = match type_ {
162                            DbValueAuthTypeV1::Anonymous => AuthType::Anonymous,
163                            DbValueAuthTypeV1::Password => AuthType::Password,
164                            DbValueAuthTypeV1::GeneratedPassword => AuthType::GeneratedPassword,
165                            DbValueAuthTypeV1::PasswordTotp => AuthType::PasswordTotp,
166                            DbValueAuthTypeV1::PasswordBackupCode => AuthType::PasswordBackupCode,
167                            DbValueAuthTypeV1::PasswordSecurityKey => AuthType::PasswordSecurityKey,
168                            DbValueAuthTypeV1::Passkey => AuthType::Passkey,
169                            DbValueAuthTypeV1::AttestedPasskey => AuthType::AttestedPasskey,
170                        };
171
172                        Some((
173                            *refer,
174                            Session {
175                                label: label.clone(),
176                                state,
177                                issued_at,
178                                issued_by,
179                                cred_id: *cred_id,
180                                scope,
181                                type_,
182                            },
183                        ))
184                    }
185                }
186            })
187            .collect();
188        Ok(Box::new(ValueSetSession { map }))
189    }
190
191    pub fn from_dbvs2(data: &[DbValueSession]) -> Result<ValueSet, OperationError> {
192        Self::from_dbv_iter(data.iter())
193    }
194
195    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
196    // types, and tuples are always foreign.
197    #[allow(clippy::should_implement_trait)]
198    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
199    where
200        T: IntoIterator<Item = (Uuid, Session)>,
201    {
202        let map = iter.into_iter().collect();
203        Some(Box::new(ValueSetSession { map }))
204    }
205}
206
207impl ValueSetT for ValueSetSession {
208    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
209        match value {
210            Value::Session(u, m) => {
211                if let BTreeEntry::Vacant(e) = self.map.entry(u) {
212                    e.insert(m);
213                    Ok(true)
214                } else {
215                    Ok(false)
216                }
217            }
218            _ => Err(OperationError::InvalidValueState),
219        }
220    }
221
222    fn clear(&mut self) {
223        self.map.clear();
224    }
225
226    fn remove(&mut self, pv: &PartialValue, cid: &Cid) -> bool {
227        match pv {
228            PartialValue::Refer(u) => {
229                if let Some(session) = self.map.get_mut(u) {
230                    if !matches!(session.state, SessionState::RevokedAt(_)) {
231                        session.state = SessionState::RevokedAt(cid.clone());
232                        true
233                    } else {
234                        false
235                    }
236                } else {
237                    false
238                }
239            }
240            _ => false,
241        }
242    }
243
244    fn purge(&mut self, cid: &Cid) -> bool {
245        for (_uuid, session) in self.map.iter_mut() {
246            // Send them all to the shadow realm
247            if !matches!(session.state, SessionState::RevokedAt(_)) {
248                session.state = SessionState::RevokedAt(cid.clone())
249            }
250        }
251        // Can't be purged since we need the cid's of revoked to persist.
252        false
253    }
254
255    fn trim(&mut self, trim_cid: &Cid) {
256        // There might be a neater way to do this with less iterations. The problem
257        // is we can't just check on what was in b/older, because then we miss
258        // trimmable content from the local map. So once the merge is complete we
259        // do a pass for trim.
260        self.map.retain(|_, session| {
261            match &session.state {
262                SessionState::RevokedAt(cid) if cid < trim_cid => {
263                    // This value is past the replication trim window and can now safely
264                    // be removed
265                    false
266                }
267                // Retain all else
268                _ => true,
269            }
270        });
271
272        // Now, assert that there are fewer or equal sessions to the limit.
273        if self.map.len() > SESSION_MAXIMUM {
274            // At this point we will force a number of sessions to be removed. This
275            // is replication safe since other replicas will also be performing
276            // the same operation on merge, since we trim by session issuance order.
277
278            // This is a "slow path". This is because we optimise session storage
279            // based on fast session lookup, so now we need to actually create an
280            // index based on time. We need to also clone here since we need to mutate
281            // self.map which would violate mut/imut.
282
283            warn!(
284                "entry has exceeded session_maximum limit ({:?}), force trimming will occur",
285                SESSION_MAXIMUM
286            );
287
288            let time_idx: BTreeMap<OffsetDateTime, Uuid> = self
289                .map
290                .iter()
291                .map(|(session_id, session)| (session.issued_at, *session_id))
292                .collect();
293
294            let to_take = self.map.len() - SESSION_MAXIMUM;
295
296            time_idx.values().take(to_take).for_each(|session_id| {
297                warn!(?session_id, "force trimmed");
298                self.map.remove(session_id);
299            });
300        }
301        // And we're done.
302    }
303
304    fn contains(&self, pv: &PartialValue) -> bool {
305        match pv {
306            PartialValue::Refer(u) => self.map.contains_key(u),
307            _ => false,
308        }
309    }
310
311    fn substring(&self, _pv: &PartialValue) -> bool {
312        false
313    }
314
315    fn startswith(&self, _pv: &PartialValue) -> bool {
316        false
317    }
318
319    fn endswith(&self, _pv: &PartialValue) -> bool {
320        false
321    }
322
323    fn lessthan(&self, _pv: &PartialValue) -> bool {
324        false
325    }
326
327    fn len(&self) -> usize {
328        self.map.len()
329    }
330
331    fn generate_idx_eq_keys(&self) -> Vec<String> {
332        self.map
333            .keys()
334            .map(|u| u.as_hyphenated().to_string())
335            .collect()
336    }
337
338    fn syntax(&self) -> SyntaxType {
339        SyntaxType::Session
340    }
341
342    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
343        true
344    }
345
346    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
347        Box::new(
348            self.map
349                .iter()
350                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
351        )
352    }
353
354    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
355        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
356            self.map
357                .iter()
358                .map(|(session_id, session)| {
359                    let (expires, revoked) = match &session.state {
360                        SessionState::ExpiresAt(odt) => (Some(*odt), None),
361                        SessionState::NeverExpires => (None, None),
362                        SessionState::RevokedAt(cid) => {
363                            let odt: OffsetDateTime = cid.into();
364                            (None, Some(odt))
365                        }
366                    };
367
368                    ScimAuthSession {
369                        id: *session_id,
370                        expires,
371                        revoked,
372
373                        issued_at: session.issued_at,
374                        issued_by: Uuid::from(&session.issued_by),
375                        credential_id: session.cred_id,
376                        auth_type: session.type_.to_string(),
377                        session_scope: session.scope.to_string(),
378                    }
379                })
380                .collect::<Vec<_>>(),
381        )))
382    }
383
384    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
385        DbValueSetV2::Session(self.to_vec_dbvs())
386    }
387
388    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
389        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
390    }
391
392    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
393        Box::new(self.map.iter().map(|(u, m)| Value::Session(*u, m.clone())))
394    }
395
396    fn equal(&self, other: &ValueSet) -> bool {
397        if let Some(other) = other.as_session_map() {
398            &self.map == other
399        } else {
400            debug_assert!(false);
401            false
402        }
403    }
404
405    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
406        if let Some(b) = other.as_session_map() {
407            // We can't just do merge maps here, we have to be aware of the
408            // session.state value and what it currently is set to.
409            for (k_other, v_other) in b.iter() {
410                if let Some(v_self) = self.map.get_mut(k_other) {
411                    // We only update if greater. This is where RevokedAt
412                    // always proceeds other states, and lower revoked
413                    // cids will always take effect.
414                    if v_other.state > v_self.state {
415                        *v_self = v_other.clone();
416                    }
417                } else {
418                    // Not present, just insert.
419                    self.map.insert(*k_other, v_other.clone());
420                }
421            }
422            Ok(())
423        } else {
424            debug_assert!(false);
425            Err(OperationError::InvalidValueState)
426        }
427    }
428
429    fn as_session_map(&self) -> Option<&BTreeMap<Uuid, Session>> {
430        Some(&self.map)
431    }
432
433    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
434        // This is what ties us as a type that can be refint checked.
435        Some(Box::new(self.map.keys().copied()))
436    }
437
438    fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
439        // If the older value has a different type - return nothing, we
440        // just take the newer value.
441        let b = older.as_session_map()?;
442        // We can't just do merge maps here, we have to be aware of the
443        // session.state value and what it currently is set to.
444        let mut map = self.map.clone();
445        for (k_other, v_other) in b.iter() {
446            if let Some(v_self) = map.get_mut(k_other) {
447                // We only update if lower. This is where RevokedAt
448                // always proceeds other states, and lower revoked
449                // cids will always take effect.
450                if v_other.state > v_self.state {
451                    *v_self = v_other.clone();
452                }
453            } else {
454                // Not present, just insert.
455                map.insert(*k_other, v_other.clone());
456            }
457        }
458
459        let mut vs = Box::new(ValueSetSession { map });
460
461        vs.trim(trim_cid);
462
463        Some(vs)
464    }
465}
466
467// == oauth2 session ==
468
469#[derive(Debug, Clone)]
470pub struct ValueSetOauth2Session {
471    map: BTreeMap<Uuid, Oauth2Session>,
472    // this is a "filter" to tell us if as rs_id is used anywhere
473    // in this set. The reason is so that we don't do O(n) searches
474    // on a refer if it's not in this set. The alternate approach is
475    // an index on these maps, but its more work to maintain for a rare
476    // situation where we actually want to query rs_uuid -> sessions.
477    rs_filter: u128,
478}
479
480impl ValueSetOauth2Session {
481    pub fn new(u: Uuid, m: Oauth2Session) -> Box<Self> {
482        let mut map = BTreeMap::new();
483        let rs_filter = m.rs_uuid.as_u128();
484        map.insert(u, m);
485        Box::new(ValueSetOauth2Session { map, rs_filter })
486    }
487
488    pub fn push(&mut self, u: Uuid, m: Oauth2Session) -> bool {
489        self.rs_filter |= m.rs_uuid.as_u128();
490        self.map.insert(u, m).is_none()
491    }
492
493    pub fn from_dbvs2(data: Vec<DbValueOauth2Session>) -> Result<ValueSet, OperationError> {
494        let mut rs_filter = u128::MIN;
495        let map = data
496            .into_iter()
497            .filter_map(|dbv| {
498                match dbv {
499                    DbValueOauth2Session::V1 {
500                        refer,
501                        parent,
502                        expiry,
503                        issued_at,
504                        rs_uuid,
505                    } => {
506                        // Convert things.
507                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
508                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
509                            .map_err(|e| {
510                                admin_error!(
511                                    ?e,
512                                    "Invalidating session {} due to invalid issued_at timestamp",
513                                    refer
514                                )
515                            })
516                            .ok()?;
517
518                        // This is a bit annoying. In the case we can't parse the optional
519                        // expiry, we need to NOT return the session so that it's immediately
520                        // invalidated. To do this we have to invert some of the options involved
521                        // here.
522                        let expiry = expiry
523                            .map(|e_inner| {
524                                OffsetDateTime::parse(&e_inner, &Rfc3339)
525                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
526                                // We now have an
527                                // Option<Result<ODT, _>>
528                            })
529                            .transpose()
530                            // Result<Option<ODT>, _>
531                            .map_err(|e| {
532                                admin_error!(
533                                    ?e,
534                                    "Invalidating session {} due to invalid expiry timestamp",
535                                    refer
536                                )
537                            })
538                            // Option<Option<ODT>>
539                            .ok()?;
540
541                        let state = expiry
542                            .map(SessionState::ExpiresAt)
543                            .unwrap_or(SessionState::NeverExpires);
544
545                        let parent = Some(parent);
546
547                        // Insert to the rs_filter.
548                        rs_filter |= rs_uuid.as_u128();
549                        Some((
550                            refer,
551                            Oauth2Session {
552                                parent,
553                                state,
554                                issued_at,
555                                rs_uuid,
556                            },
557                        ))
558                    }
559                    DbValueOauth2Session::V2 {
560                        refer,
561                        parent,
562                        state,
563                        issued_at,
564                        rs_uuid,
565                    } => {
566                        // Convert things.
567                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
568                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
569                            .map_err(|e| {
570                                admin_error!(
571                                    ?e,
572                                    "Invalidating session {} due to invalid issued_at timestamp",
573                                    refer
574                                )
575                            })
576                            .ok()?;
577
578                        let state = match state {
579                            DbValueSessionStateV1::ExpiresAt(e_inner) => {
580                                OffsetDateTime::parse(&e_inner, &Rfc3339)
581                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
582                                    .map(SessionState::ExpiresAt)
583                                    .map_err(|e| {
584                                        admin_error!(
585                                    ?e,
586                                    "Invalidating session {} due to invalid expiry timestamp",
587                                    refer
588                                )
589                                    })
590                                    .ok()?
591                            }
592                            DbValueSessionStateV1::Never => SessionState::NeverExpires,
593                            DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
594                                s_uuid: dc.server_id,
595                                ts: dc.timestamp,
596                            }),
597                        };
598
599                        rs_filter |= rs_uuid.as_u128();
600
601                        let parent = Some(parent);
602
603                        Some((
604                            refer,
605                            Oauth2Session {
606                                parent,
607                                state,
608                                issued_at,
609                                rs_uuid,
610                            },
611                        ))
612                    } // End V2
613                    DbValueOauth2Session::V3 {
614                        refer,
615                        parent,
616                        state,
617                        issued_at,
618                        rs_uuid,
619                    } => {
620                        // Convert things.
621                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
622                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
623                            .map_err(|e| {
624                                admin_error!(
625                                    ?e,
626                                    "Invalidating session {} due to invalid issued_at timestamp",
627                                    refer
628                                )
629                            })
630                            .ok()?;
631
632                        let state = match state {
633                            DbValueSessionStateV1::ExpiresAt(e_inner) => {
634                                OffsetDateTime::parse(&e_inner, &Rfc3339)
635                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
636                                    .map(SessionState::ExpiresAt)
637                                    .map_err(|e| {
638                                        admin_error!(
639                                    ?e,
640                                    "Invalidating session {} due to invalid expiry timestamp",
641                                    refer
642                                )
643                                    })
644                                    .ok()?
645                            }
646                            DbValueSessionStateV1::Never => SessionState::NeverExpires,
647                            DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
648                                s_uuid: dc.server_id,
649                                ts: dc.timestamp,
650                            }),
651                        };
652
653                        rs_filter |= rs_uuid.as_u128();
654
655                        Some((
656                            refer,
657                            Oauth2Session {
658                                parent,
659                                state,
660                                issued_at,
661                                rs_uuid,
662                            },
663                        ))
664                    } // End V3
665                }
666            })
667            .collect();
668        Ok(Box::new(ValueSetOauth2Session { map, rs_filter }))
669    }
670
671    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
672    // types, and tuples are always foreign.
673    #[allow(clippy::should_implement_trait)]
674    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
675    where
676        T: IntoIterator<Item = (Uuid, Oauth2Session)>,
677    {
678        let mut rs_filter = u128::MIN;
679        let map = iter
680            .into_iter()
681            .map(|(u, m)| {
682                rs_filter |= m.rs_uuid.as_u128();
683                (u, m)
684            })
685            .collect();
686        Some(Box::new(ValueSetOauth2Session { map, rs_filter }))
687    }
688}
689
690impl ValueSetT for ValueSetOauth2Session {
691    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
692        match value {
693            Value::Oauth2Session(u, m) => {
694                // Unlike other types, this allows overwriting as oauth2 sessions
695                // can be *extended* in time length.
696                match self.map.entry(u) {
697                    BTreeEntry::Vacant(e) => {
698                        self.rs_filter |= m.rs_uuid.as_u128();
699                        e.insert(m);
700                        Ok(true)
701                    }
702                    BTreeEntry::Occupied(mut e) => {
703                        let e_v = e.get_mut();
704                        if m.state > e_v.state {
705                            // Replace if the state has higher priority.
706                            *e_v = m;
707                            Ok(true)
708                        } else {
709                            // Else take no action.
710                            Ok(false)
711                        }
712                    }
713                }
714            }
715            _ => Err(OperationError::InvalidValueState),
716        }
717    }
718
719    fn clear(&mut self) {
720        self.rs_filter = u128::MIN;
721        self.map.clear();
722    }
723
724    fn remove(&mut self, pv: &PartialValue, cid: &Cid) -> bool {
725        match pv {
726            PartialValue::Refer(u) => {
727                if let Some(session) = self.map.get_mut(u) {
728                    if !matches!(session.state, SessionState::RevokedAt(_)) {
729                        session.state = SessionState::RevokedAt(cid.clone());
730                        true
731                    } else {
732                        false
733                    }
734                } else {
735                    // What if it's an rs_uuid?
736                    let u_int = u.as_u128();
737                    if self.rs_filter & u_int == u_int {
738                        // It's there, so we need to do a more costly revoke over the values
739                        // that are present.
740                        let mut removed = false;
741                        self.map.values_mut().for_each(|session| {
742                            if session.rs_uuid == *u {
743                                session.state = SessionState::RevokedAt(cid.clone());
744                                removed = true;
745                            }
746                        });
747                        removed
748                    } else {
749                        // It's not in the rs_filter or the map, false.
750                        false
751                    }
752                }
753            }
754            _ => false,
755        }
756    }
757
758    fn purge(&mut self, cid: &Cid) -> bool {
759        for (_uuid, session) in self.map.iter_mut() {
760            // Send them all to the shadow realm
761            if !matches!(session.state, SessionState::RevokedAt(_)) {
762                session.state = SessionState::RevokedAt(cid.clone())
763            }
764        }
765        // Can't be purged since we need the cid's of revoked to persist.
766        false
767    }
768
769    fn trim(&mut self, trim_cid: &Cid) {
770        // There might be a neater way to do this with less iterations. The problem
771        // is we can't just check on what was in b/older, because then we miss
772        // trimmable content from the local map. So once the merge is complete we
773        // do a pass for trim.
774        self.map.retain(|_, session| {
775            match &session.state {
776                SessionState::RevokedAt(cid) if cid < trim_cid => {
777                    // This value is past the replication trim window and can now safely
778                    // be removed
779                    false
780                }
781                // Retain all else
782                _ => true,
783            }
784        })
785    }
786
787    fn contains(&self, pv: &PartialValue) -> bool {
788        match pv {
789            PartialValue::Refer(u) => {
790                self.map.contains_key(u) || {
791                    let u_int = u.as_u128();
792                    if self.rs_filter & u_int == u_int {
793                        self.map.values().any(|session| {
794                            session.rs_uuid == *u
795                                && !matches!(session.state, SessionState::RevokedAt(_))
796                        })
797                    } else {
798                        false
799                    }
800                }
801            }
802            _ => false,
803        }
804    }
805
806    fn substring(&self, _pv: &PartialValue) -> bool {
807        false
808    }
809
810    fn startswith(&self, _pv: &PartialValue) -> bool {
811        false
812    }
813
814    fn endswith(&self, _pv: &PartialValue) -> bool {
815        false
816    }
817
818    fn lessthan(&self, _pv: &PartialValue) -> bool {
819        false
820    }
821
822    fn len(&self) -> usize {
823        self.map.len()
824    }
825
826    fn generate_idx_eq_keys(&self) -> Vec<String> {
827        // Allocate twice as much for worst-case when every session is
828        // a unique rs-uuid to prevent re-allocs.
829        let mut idx_keys = Vec::with_capacity(self.map.len() * 2);
830        for (k, v) in self.map.iter() {
831            idx_keys.push(k.as_hyphenated().to_string());
832            idx_keys.push(v.rs_uuid.as_hyphenated().to_string());
833        }
834        idx_keys.sort_unstable();
835        idx_keys.dedup();
836        idx_keys
837    }
838
839    fn syntax(&self) -> SyntaxType {
840        SyntaxType::Oauth2Session
841    }
842
843    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
844        true
845    }
846
847    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
848        Box::new(
849            self.map
850                .iter()
851                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
852        )
853    }
854
855    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
856        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
857            self.map
858                .iter()
859                .map(|(session_id, session)| {
860                    let (expires, revoked) = match &session.state {
861                        SessionState::ExpiresAt(odt) => (Some(*odt), None),
862                        SessionState::NeverExpires => (None, None),
863                        SessionState::RevokedAt(cid) => {
864                            let odt: OffsetDateTime = cid.into();
865                            (None, Some(odt))
866                        }
867                    };
868
869                    ScimOAuth2Session {
870                        id: *session_id,
871                        parent_id: session.parent,
872                        client_id: session.rs_uuid,
873                        issued_at: session.issued_at,
874                        expires,
875                        revoked,
876                    }
877                })
878                .collect::<Vec<_>>(),
879        )))
880    }
881
882    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
883        DbValueSetV2::Oauth2Session(
884            self.map
885                .iter()
886                .map(|(u, m)| DbValueOauth2Session::V3 {
887                    refer: *u,
888                    parent: m.parent,
889                    state: match &m.state {
890                        SessionState::ExpiresAt(odt) => {
891                            debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
892                            #[allow(clippy::expect_used)]
893                            odt.format(&Rfc3339)
894                                .map(DbValueSessionStateV1::ExpiresAt)
895                                .expect("Failed to format timestamp into RFC3339!")
896                        }
897                        SessionState::NeverExpires => DbValueSessionStateV1::Never,
898                        SessionState::RevokedAt(c) => DbValueSessionStateV1::RevokedAt(DbCidV1 {
899                            server_id: c.s_uuid,
900                            timestamp: c.ts,
901                        }),
902                    },
903                    issued_at: {
904                        debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
905                        #[allow(clippy::expect_used)]
906                        m.issued_at
907                            .format(&Rfc3339)
908                            .expect("Failed to format timestamp as RFC3339")
909                    },
910                    rs_uuid: m.rs_uuid,
911                })
912                .collect(),
913        )
914    }
915
916    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
917        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
918    }
919
920    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
921        Box::new(
922            self.map
923                .iter()
924                .map(|(u, m)| Value::Oauth2Session(*u, m.clone())),
925        )
926    }
927
928    fn equal(&self, other: &ValueSet) -> bool {
929        if let Some(other) = other.as_oauth2session_map() {
930            &self.map == other
931        } else {
932            debug_assert!(false);
933            false
934        }
935    }
936
937    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
938        if let Some(b) = other.as_oauth2session_map() {
939            // We can't just do merge maps here, we have to be aware of the
940            // session.state value and what it currently is set to. We also
941            // need to make sure the rs_filter is updated too!
942            for (k_other, v_other) in b.iter() {
943                if let Some(v_self) = self.map.get_mut(k_other) {
944                    // We only update if lower. This is where RevokedAt
945                    // always proceeds other states, and lower revoked
946                    // cids will always take effect.
947                    if v_other.state > v_self.state {
948                        *v_self = v_other.clone();
949                    }
950                } else {
951                    // Update the rs_filter!
952                    self.rs_filter |= v_other.rs_uuid.as_u128();
953                    // Not present, just insert.
954                    self.map.insert(*k_other, v_other.clone());
955                }
956            }
957            Ok(())
958        } else {
959            debug_assert!(false);
960            Err(OperationError::InvalidValueState)
961        }
962    }
963
964    fn as_oauth2session_map(&self) -> Option<&BTreeMap<Uuid, Oauth2Session>> {
965        Some(&self.map)
966    }
967
968    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
969        // This is what ties us as a type that can be refint checked. We need to
970        // bind to our resource servers, not our ids!
971        Some(Box::new(self.map.values().map(|m| &m.rs_uuid).copied()))
972    }
973
974    fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
975        if let Some(b) = older.as_oauth2session_map() {
976            // We can't just do merge maps here, we have to be aware of the
977            // session.state value and what it currently is set to.
978            let mut map = self.map.clone();
979            let mut rs_filter = self.rs_filter;
980            for (k_other, v_other) in b.iter() {
981                if let Some(v_self) = map.get_mut(k_other) {
982                    // We only update if greater. This is where RevokedAt
983                    // always proceeds other states, and lower revoked
984                    // cids will always take effect.
985                    if v_other.state > v_self.state {
986                        *v_self = v_other.clone();
987                    }
988                } else {
989                    // Not present, just insert.
990                    rs_filter |= v_other.rs_uuid.as_u128();
991                    map.insert(*k_other, v_other.clone());
992                }
993            }
994
995            let mut vs = Box::new(ValueSetOauth2Session { map, rs_filter });
996
997            vs.trim(trim_cid);
998
999            Some(vs)
1000        } else {
1001            // The older value has a different type - return nothing, we
1002            // just take the newer value.
1003            None
1004        }
1005    }
1006}
1007
1008#[derive(Debug, Clone)]
1009pub struct ValueSetApiToken {
1010    map: BTreeMap<Uuid, ApiToken>,
1011}
1012
1013impl ValueSetApiToken {
1014    pub fn new(u: Uuid, m: ApiToken) -> Box<Self> {
1015        let mut map = BTreeMap::new();
1016        map.insert(u, m);
1017        Box::new(ValueSetApiToken { map })
1018    }
1019
1020    pub fn push(&mut self, u: Uuid, m: ApiToken) -> bool {
1021        self.map.insert(u, m).is_none()
1022    }
1023
1024    pub fn from_dbvs2(data: Vec<DbValueApiToken>) -> Result<ValueSet, OperationError> {
1025        let map = data
1026            .into_iter()
1027            .filter_map(|dbv| {
1028                match dbv {
1029                    DbValueApiToken::V1 {
1030                        refer,
1031                        label,
1032                        expiry,
1033                        issued_at,
1034                        issued_by,
1035                        scope,
1036                    } => {
1037                        // Convert things.
1038                        let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
1039                            .map(|odt| odt.to_offset(time::UtcOffset::UTC))
1040                            .map_err(|e| {
1041                                admin_error!(
1042                                    ?e,
1043                                    "Invalidating api token {} due to invalid issued_at timestamp",
1044                                    refer
1045                                )
1046                            })
1047                            .ok()?;
1048
1049                        // This is a bit annoying. In the case we can't parse the optional
1050                        // expiry, we need to NOT return the session so that it's immediately
1051                        // invalidated. To do this we have to invert some of the options involved
1052                        // here.
1053                        let expiry = expiry
1054                            .map(|e_inner| {
1055                                OffsetDateTime::parse(&e_inner, &Rfc3339)
1056                                    .map(|odt| odt.to_offset(time::UtcOffset::UTC))
1057                                // We now have an
1058                                // Option<Result<ODT, _>>
1059                            })
1060                            .transpose()
1061                            // Result<Option<ODT>, _>
1062                            .map_err(|e| {
1063                                admin_error!(
1064                                    ?e,
1065                                    "Invalidating api token {} due to invalid expiry timestamp",
1066                                    refer
1067                                )
1068                            })
1069                            // Option<Option<ODT>>
1070                            .ok()?;
1071
1072                        let issued_by = match issued_by {
1073                            DbValueIdentityId::V1Internal => IdentityId::Internal,
1074                            DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
1075                            DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
1076                        };
1077
1078                        let scope = match scope {
1079                            DbValueApiTokenScopeV1::ReadOnly => ApiTokenScope::ReadOnly,
1080                            DbValueApiTokenScopeV1::ReadWrite => ApiTokenScope::ReadWrite,
1081                            DbValueApiTokenScopeV1::Synchronise => ApiTokenScope::Synchronise,
1082                        };
1083
1084                        Some((
1085                            refer,
1086                            ApiToken {
1087                                label,
1088                                expiry,
1089                                issued_at,
1090                                issued_by,
1091                                scope,
1092                            },
1093                        ))
1094                    }
1095                }
1096            })
1097            .collect();
1098        Ok(Box::new(ValueSetApiToken { map }))
1099    }
1100
1101    // We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
1102    // types, and tuples are always foreign.
1103    #[allow(clippy::should_implement_trait)]
1104    pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
1105    where
1106        T: IntoIterator<Item = (Uuid, ApiToken)>,
1107    {
1108        let map = iter.into_iter().collect();
1109        Some(Box::new(ValueSetApiToken { map }))
1110    }
1111}
1112
1113impl ValueSetT for ValueSetApiToken {
1114    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
1115        match value {
1116            Value::ApiToken(u, m) => {
1117                if let BTreeEntry::Vacant(e) = self.map.entry(u) {
1118                    e.insert(m);
1119                    Ok(true)
1120                } else {
1121                    Ok(false)
1122                }
1123            }
1124            _ => Err(OperationError::InvalidValueState),
1125        }
1126    }
1127
1128    fn clear(&mut self) {
1129        self.map.clear();
1130    }
1131
1132    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
1133        match pv {
1134            PartialValue::Refer(u) => self.map.remove(u).is_some(),
1135            _ => false,
1136        }
1137    }
1138
1139    fn purge(&mut self, _cid: &Cid) -> bool {
1140        // Could consider making this a TS capable entry.
1141        true
1142    }
1143
1144    fn contains(&self, pv: &PartialValue) -> bool {
1145        match pv {
1146            PartialValue::Refer(u) => self.map.contains_key(u),
1147            _ => false,
1148        }
1149    }
1150
1151    fn substring(&self, _pv: &PartialValue) -> bool {
1152        false
1153    }
1154
1155    fn startswith(&self, _pv: &PartialValue) -> bool {
1156        false
1157    }
1158
1159    fn endswith(&self, _pv: &PartialValue) -> bool {
1160        false
1161    }
1162
1163    fn lessthan(&self, _pv: &PartialValue) -> bool {
1164        false
1165    }
1166
1167    fn len(&self) -> usize {
1168        self.map.len()
1169    }
1170
1171    fn generate_idx_eq_keys(&self) -> Vec<String> {
1172        self.map
1173            .keys()
1174            .map(|u| u.as_hyphenated().to_string())
1175            .collect()
1176    }
1177
1178    fn syntax(&self) -> SyntaxType {
1179        SyntaxType::ApiToken
1180    }
1181
1182    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
1183        self.map.iter().all(|(_, at)| {
1184            Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label)
1185        })
1186    }
1187
1188    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
1189        Box::new(
1190            self.map
1191                .iter()
1192                .map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
1193        )
1194    }
1195
1196    fn to_scim_value(&self) -> Option<ScimResolveStatus> {
1197        Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
1198            self.map
1199                .iter()
1200                .map(|(token_id, token)| ScimApiToken {
1201                    id: *token_id,
1202                    label: token.label.clone(),
1203                    issued_by: Uuid::from(&token.issued_by),
1204                    issued_at: token.issued_at,
1205                    expires: token.expiry,
1206                    scope: token.scope.to_string(),
1207                })
1208                .collect::<Vec<_>>(),
1209        )))
1210    }
1211
1212    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
1213        DbValueSetV2::ApiToken(
1214            self.map
1215                .iter()
1216                .map(|(u, m)| DbValueApiToken::V1 {
1217                    refer: *u,
1218                    label: m.label.clone(),
1219                    expiry: m.expiry.map(|odt| {
1220                        debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
1221                        #[allow(clippy::expect_used)]
1222                        odt.format(&Rfc3339)
1223                            .expect("Failed to format timestamp into RFC3339")
1224                    }),
1225                    issued_at: {
1226                        debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
1227                        #[allow(clippy::expect_used)]
1228                        m.issued_at
1229                            .format(&Rfc3339)
1230                            .expect("Failed to format timestamp into RFC3339")
1231                    },
1232                    issued_by: match m.issued_by {
1233                        IdentityId::Internal => DbValueIdentityId::V1Internal,
1234                        IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
1235                        IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
1236                    },
1237                    scope: match m.scope {
1238                        ApiTokenScope::ReadOnly => DbValueApiTokenScopeV1::ReadOnly,
1239                        ApiTokenScope::ReadWrite => DbValueApiTokenScopeV1::ReadWrite,
1240                        ApiTokenScope::Synchronise => DbValueApiTokenScopeV1::Synchronise,
1241                    },
1242                })
1243                .collect(),
1244        )
1245    }
1246
1247    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
1248        Box::new(self.map.keys().cloned().map(PartialValue::Refer))
1249    }
1250
1251    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
1252        Box::new(self.map.iter().map(|(u, m)| Value::ApiToken(*u, m.clone())))
1253    }
1254
1255    fn equal(&self, other: &ValueSet) -> bool {
1256        if let Some(other) = other.as_apitoken_map() {
1257            &self.map == other
1258        } else {
1259            debug_assert!(false);
1260            false
1261        }
1262    }
1263
1264    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
1265        if let Some(b) = other.as_apitoken_map() {
1266            mergemaps!(self.map, b)
1267        } else {
1268            debug_assert!(false);
1269            Err(OperationError::InvalidValueState)
1270        }
1271    }
1272
1273    fn as_apitoken_map(&self) -> Option<&BTreeMap<Uuid, ApiToken>> {
1274        Some(&self.map)
1275    }
1276
1277    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
1278        // This is what ties us as a type that can be refint checked.
1279        Some(Box::new(self.map.keys().copied()))
1280    }
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285    use super::{ValueSetOauth2Session, ValueSetSession, SESSION_MAXIMUM};
1286    use crate::prelude::ValueSet;
1287    use crate::prelude::{IdentityId, SessionScope, Uuid};
1288    use crate::repl::cid::Cid;
1289    use crate::value::{AuthType, Oauth2Session, Session, SessionState};
1290    use time::OffsetDateTime;
1291
1292    #[test]
1293    fn test_valueset_session_purge() {
1294        let s_uuid = Uuid::new_v4();
1295
1296        let mut vs: ValueSet = ValueSetSession::new(
1297            s_uuid,
1298            Session {
1299                label: "hacks".to_string(),
1300                state: SessionState::NeverExpires,
1301                issued_at: OffsetDateTime::now_utc(),
1302                issued_by: IdentityId::Internal,
1303                cred_id: Uuid::new_v4(),
1304                scope: SessionScope::ReadOnly,
1305                type_: AuthType::Passkey,
1306            },
1307        );
1308
1309        let zero_cid = Cid::new_zero();
1310
1311        // Simulate session revocation.
1312        vs.purge(&zero_cid);
1313
1314        assert_eq!(vs.len(), 1);
1315
1316        let session = vs
1317            .as_session_map()
1318            .and_then(|map| map.get(&s_uuid))
1319            .expect("Unable to locate session");
1320
1321        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1322    }
1323
1324    #[test]
1325    fn test_valueset_session_merge_left() {
1326        let s_uuid = Uuid::new_v4();
1327        let zero_cid = Cid::new_zero();
1328
1329        let mut vs_a: ValueSet = ValueSetSession::new(
1330            s_uuid,
1331            Session {
1332                label: "hacks".to_string(),
1333                state: SessionState::NeverExpires,
1334                issued_at: OffsetDateTime::now_utc(),
1335                issued_by: IdentityId::Internal,
1336                cred_id: Uuid::new_v4(),
1337                scope: SessionScope::ReadOnly,
1338                type_: AuthType::Passkey,
1339            },
1340        );
1341
1342        let vs_b: ValueSet = ValueSetSession::new(
1343            s_uuid,
1344            Session {
1345                label: "hacks".to_string(),
1346                state: SessionState::RevokedAt(zero_cid.clone()),
1347                issued_at: OffsetDateTime::now_utc(),
1348                issued_by: IdentityId::Internal,
1349                cred_id: Uuid::new_v4(),
1350                scope: SessionScope::ReadOnly,
1351                type_: AuthType::Passkey,
1352            },
1353        );
1354
1355        vs_a.merge(&vs_b).expect("failed to merge");
1356
1357        let session = vs_a
1358            .as_session_map()
1359            .and_then(|map| map.get(&s_uuid))
1360            .expect("Unable to locate session");
1361
1362        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1363    }
1364
1365    #[test]
1366    fn test_valueset_session_merge_right() {
1367        let s_uuid = Uuid::new_v4();
1368        let zero_cid = Cid::new_zero();
1369
1370        let vs_a: ValueSet = ValueSetSession::new(
1371            s_uuid,
1372            Session {
1373                label: "hacks".to_string(),
1374                state: SessionState::NeverExpires,
1375                issued_at: OffsetDateTime::now_utc(),
1376                issued_by: IdentityId::Internal,
1377                cred_id: Uuid::new_v4(),
1378                scope: SessionScope::ReadOnly,
1379                type_: AuthType::Passkey,
1380            },
1381        );
1382
1383        let mut vs_b: ValueSet = ValueSetSession::new(
1384            s_uuid,
1385            Session {
1386                label: "hacks".to_string(),
1387                state: SessionState::RevokedAt(zero_cid.clone()),
1388                issued_at: OffsetDateTime::now_utc(),
1389                issued_by: IdentityId::Internal,
1390                cred_id: Uuid::new_v4(),
1391                scope: SessionScope::ReadOnly,
1392                type_: AuthType::Passkey,
1393            },
1394        );
1395
1396        // Note - inverse order!
1397        vs_b.merge(&vs_a).expect("failed to merge");
1398
1399        let session = vs_b
1400            .as_session_map()
1401            .and_then(|map| map.get(&s_uuid))
1402            .expect("Unable to locate session");
1403
1404        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1405    }
1406
1407    #[test]
1408    fn test_valueset_session_repl_merge_left() {
1409        let s_uuid = Uuid::new_v4();
1410        let r_uuid = Uuid::new_v4();
1411        let zero_cid = Cid::new_zero();
1412        let one_cid = Cid::new_count(1);
1413
1414        let vs_a: ValueSet = ValueSetSession::new(
1415            s_uuid,
1416            Session {
1417                label: "hacks".to_string(),
1418                state: SessionState::NeverExpires,
1419                issued_at: OffsetDateTime::now_utc(),
1420                issued_by: IdentityId::Internal,
1421                cred_id: Uuid::new_v4(),
1422                scope: SessionScope::ReadOnly,
1423                type_: AuthType::Passkey,
1424            },
1425        );
1426
1427        let vs_b: ValueSet = ValueSetSession::from_iter([
1428            (
1429                s_uuid,
1430                Session {
1431                    label: "hacks".to_string(),
1432                    state: SessionState::RevokedAt(one_cid.clone()),
1433                    issued_at: OffsetDateTime::now_utc(),
1434                    issued_by: IdentityId::Internal,
1435                    cred_id: Uuid::new_v4(),
1436                    scope: SessionScope::ReadOnly,
1437                    type_: AuthType::Passkey,
1438                },
1439            ),
1440            (
1441                r_uuid,
1442                Session {
1443                    label: "hacks".to_string(),
1444                    state: SessionState::RevokedAt(zero_cid.clone()),
1445                    issued_at: OffsetDateTime::now_utc(),
1446                    issued_by: IdentityId::Internal,
1447                    cred_id: Uuid::new_v4(),
1448                    scope: SessionScope::ReadOnly,
1449                    type_: AuthType::Passkey,
1450                },
1451            ),
1452        ])
1453        .expect("Unable to build valueset session");
1454
1455        let r_vs = vs_a
1456            .repl_merge_valueset(&vs_b, &one_cid)
1457            .expect("failed to merge");
1458
1459        let sessions = r_vs.as_session_map().expect("Unable to locate sessions");
1460
1461        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1462
1463        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1464
1465        assert!(!sessions.contains_key(&r_uuid));
1466    }
1467
1468    #[test]
1469    fn test_valueset_session_repl_merge_right() {
1470        let s_uuid = Uuid::new_v4();
1471        let r_uuid = Uuid::new_v4();
1472        let zero_cid = Cid::new_zero();
1473        let one_cid = Cid::new_count(1);
1474
1475        let vs_a: ValueSet = ValueSetSession::new(
1476            s_uuid,
1477            Session {
1478                label: "hacks".to_string(),
1479                state: SessionState::NeverExpires,
1480                issued_at: OffsetDateTime::now_utc(),
1481                issued_by: IdentityId::Internal,
1482                cred_id: Uuid::new_v4(),
1483                scope: SessionScope::ReadOnly,
1484                type_: AuthType::Passkey,
1485            },
1486        );
1487
1488        let vs_b: ValueSet = ValueSetSession::from_iter([
1489            (
1490                s_uuid,
1491                Session {
1492                    label: "hacks".to_string(),
1493                    state: SessionState::RevokedAt(one_cid.clone()),
1494                    issued_at: OffsetDateTime::now_utc(),
1495                    issued_by: IdentityId::Internal,
1496                    cred_id: Uuid::new_v4(),
1497                    scope: SessionScope::ReadOnly,
1498                    type_: AuthType::Passkey,
1499                },
1500            ),
1501            (
1502                r_uuid,
1503                Session {
1504                    label: "hacks".to_string(),
1505                    state: SessionState::RevokedAt(zero_cid.clone()),
1506                    issued_at: OffsetDateTime::now_utc(),
1507                    issued_by: IdentityId::Internal,
1508                    cred_id: Uuid::new_v4(),
1509                    scope: SessionScope::ReadOnly,
1510                    type_: AuthType::Passkey,
1511                },
1512            ),
1513        ])
1514        .expect("Unable to build valueset session");
1515
1516        // Note - inverse order!
1517        let r_vs = vs_b
1518            .repl_merge_valueset(&vs_a, &one_cid)
1519            .expect("failed to merge");
1520
1521        let sessions = r_vs.as_session_map().expect("Unable to locate sessions");
1522
1523        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1524
1525        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1526
1527        assert!(!sessions.contains_key(&r_uuid));
1528    }
1529
1530    #[test]
1531    fn test_valueset_session_repl_trim() {
1532        let zero_uuid = Uuid::new_v4();
1533        let zero_cid = Cid::new_zero();
1534        let one_uuid = Uuid::new_v4();
1535        let one_cid = Cid::new_count(1);
1536        let two_uuid = Uuid::new_v4();
1537        let two_cid = Cid::new_count(2);
1538
1539        let mut vs_a: ValueSet = ValueSetSession::from_iter([
1540            (
1541                zero_uuid,
1542                Session {
1543                    state: SessionState::RevokedAt(zero_cid),
1544                    label: "hacks".to_string(),
1545                    issued_at: OffsetDateTime::now_utc(),
1546                    issued_by: IdentityId::Internal,
1547                    cred_id: Uuid::new_v4(),
1548                    scope: SessionScope::ReadOnly,
1549                    type_: AuthType::Passkey,
1550                },
1551            ),
1552            (
1553                one_uuid,
1554                Session {
1555                    state: SessionState::RevokedAt(one_cid),
1556                    label: "hacks".to_string(),
1557                    issued_at: OffsetDateTime::now_utc(),
1558                    issued_by: IdentityId::Internal,
1559                    cred_id: Uuid::new_v4(),
1560                    scope: SessionScope::ReadOnly,
1561                    type_: AuthType::Passkey,
1562                },
1563            ),
1564            (
1565                two_uuid,
1566                Session {
1567                    state: SessionState::RevokedAt(two_cid.clone()),
1568                    label: "hacks".to_string(),
1569                    issued_at: OffsetDateTime::now_utc(),
1570                    issued_by: IdentityId::Internal,
1571                    cred_id: Uuid::new_v4(),
1572                    scope: SessionScope::ReadOnly,
1573                    type_: AuthType::Passkey,
1574                },
1575            ),
1576        ])
1577        .unwrap();
1578
1579        vs_a.trim(&two_cid);
1580
1581        let sessions = vs_a.as_session_map().expect("Unable to locate session");
1582
1583        assert!(!sessions.contains_key(&zero_uuid));
1584        assert!(!sessions.contains_key(&one_uuid));
1585        assert!(sessions.contains_key(&two_uuid));
1586    }
1587
1588    #[test]
1589    fn test_valueset_session_limit_trim() {
1590        // Create a session that will be trimmed.
1591        let zero_uuid = Uuid::new_v4();
1592        let zero_cid = Cid::new_zero();
1593        let issued_at = OffsetDateTime::UNIX_EPOCH;
1594
1595        let session_iter = std::iter::once((
1596            zero_uuid,
1597            Session {
1598                state: SessionState::NeverExpires,
1599                label: "hacks".to_string(),
1600                issued_at,
1601                issued_by: IdentityId::Internal,
1602                cred_id: Uuid::new_v4(),
1603                scope: SessionScope::ReadOnly,
1604                type_: AuthType::Passkey,
1605            },
1606        ))
1607        .chain((0..SESSION_MAXIMUM).map(|_| {
1608            (
1609                Uuid::new_v4(),
1610                Session {
1611                    state: SessionState::NeverExpires,
1612                    label: "hacks".to_string(),
1613                    issued_at: OffsetDateTime::now_utc(),
1614                    issued_by: IdentityId::Internal,
1615                    cred_id: Uuid::new_v4(),
1616                    scope: SessionScope::ReadOnly,
1617                    type_: AuthType::Passkey,
1618                },
1619            )
1620        }));
1621
1622        let mut vs_a: ValueSet = ValueSetSession::from_iter(session_iter).unwrap();
1623
1624        assert!(vs_a.len() > SESSION_MAXIMUM);
1625
1626        vs_a.trim(&zero_cid);
1627
1628        assert_eq!(vs_a.len(), SESSION_MAXIMUM);
1629
1630        let sessions = vs_a.as_session_map().expect("Unable to access sessions");
1631
1632        assert!(!sessions.contains_key(&zero_uuid));
1633    }
1634
1635    #[test]
1636    fn test_valueset_oauth2_session_purge() {
1637        let s_uuid = Uuid::new_v4();
1638        let mut vs: ValueSet = ValueSetOauth2Session::new(
1639            s_uuid,
1640            Oauth2Session {
1641                state: SessionState::NeverExpires,
1642                issued_at: OffsetDateTime::now_utc(),
1643                parent: Some(Uuid::new_v4()),
1644                rs_uuid: Uuid::new_v4(),
1645            },
1646        );
1647
1648        let zero_cid = Cid::new_zero();
1649
1650        // Simulate session revocation.
1651        vs.purge(&zero_cid);
1652
1653        assert_eq!(vs.len(), 1);
1654
1655        let session = vs
1656            .as_oauth2session_map()
1657            .and_then(|map| map.get(&s_uuid))
1658            .expect("Unable to locate session");
1659
1660        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1661    }
1662
1663    #[test]
1664    fn test_valueset_oauth2_session_merge_left() {
1665        let s_uuid = Uuid::new_v4();
1666        let zero_cid = Cid::new_zero();
1667
1668        let mut vs_a: ValueSet = ValueSetOauth2Session::new(
1669            s_uuid,
1670            Oauth2Session {
1671                state: SessionState::NeverExpires,
1672                issued_at: OffsetDateTime::now_utc(),
1673                parent: Some(Uuid::new_v4()),
1674                rs_uuid: Uuid::new_v4(),
1675            },
1676        );
1677
1678        let vs_b: ValueSet = ValueSetOauth2Session::new(
1679            s_uuid,
1680            Oauth2Session {
1681                state: SessionState::RevokedAt(zero_cid.clone()),
1682                issued_at: OffsetDateTime::now_utc(),
1683                parent: Some(Uuid::new_v4()),
1684                rs_uuid: Uuid::new_v4(),
1685            },
1686        );
1687
1688        vs_a.merge(&vs_b).expect("failed to merge");
1689
1690        let session = vs_a
1691            .as_oauth2session_map()
1692            .and_then(|map| map.get(&s_uuid))
1693            .expect("Unable to locate session");
1694
1695        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1696    }
1697
1698    #[test]
1699    fn test_valueset_oauth2_session_merge_right() {
1700        let s_uuid = Uuid::new_v4();
1701        let zero_cid = Cid::new_zero();
1702
1703        let vs_a: ValueSet = ValueSetOauth2Session::new(
1704            s_uuid,
1705            Oauth2Session {
1706                state: SessionState::NeverExpires,
1707                issued_at: OffsetDateTime::now_utc(),
1708                parent: Some(Uuid::new_v4()),
1709                rs_uuid: Uuid::new_v4(),
1710            },
1711        );
1712
1713        let mut vs_b: ValueSet = ValueSetOauth2Session::new(
1714            s_uuid,
1715            Oauth2Session {
1716                state: SessionState::RevokedAt(zero_cid.clone()),
1717                issued_at: OffsetDateTime::now_utc(),
1718                parent: Some(Uuid::new_v4()),
1719                rs_uuid: Uuid::new_v4(),
1720            },
1721        );
1722
1723        // Note inverse order
1724        vs_b.merge(&vs_a).expect("failed to merge");
1725
1726        let session = vs_b
1727            .as_oauth2session_map()
1728            .and_then(|map| map.get(&s_uuid))
1729            .expect("Unable to locate session");
1730
1731        assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
1732    }
1733
1734    #[test]
1735    fn test_valueset_oauth2_session_repl_merge_left() {
1736        let s_uuid = Uuid::new_v4();
1737        let r_uuid = Uuid::new_v4();
1738        let zero_cid = Cid::new_zero();
1739        let one_cid = Cid::new_count(1);
1740
1741        let vs_a: ValueSet = ValueSetOauth2Session::new(
1742            s_uuid,
1743            Oauth2Session {
1744                state: SessionState::NeverExpires,
1745                issued_at: OffsetDateTime::now_utc(),
1746                parent: Some(Uuid::new_v4()),
1747                rs_uuid: Uuid::new_v4(),
1748            },
1749        );
1750
1751        let vs_b: ValueSet = ValueSetOauth2Session::from_iter([
1752            (
1753                s_uuid,
1754                Oauth2Session {
1755                    state: SessionState::RevokedAt(one_cid.clone()),
1756                    issued_at: OffsetDateTime::now_utc(),
1757                    parent: Some(Uuid::new_v4()),
1758                    rs_uuid: Uuid::new_v4(),
1759                },
1760            ),
1761            (
1762                r_uuid,
1763                Oauth2Session {
1764                    state: SessionState::RevokedAt(zero_cid.clone()),
1765                    issued_at: OffsetDateTime::now_utc(),
1766                    parent: Some(Uuid::new_v4()),
1767                    rs_uuid: Uuid::new_v4(),
1768                },
1769            ),
1770        ])
1771        .expect("Unable to build valueset oauth2 session");
1772
1773        let r_vs = vs_a
1774            .repl_merge_valueset(&vs_b, &one_cid)
1775            .expect("failed to merge");
1776
1777        let sessions = r_vs
1778            .as_oauth2session_map()
1779            .expect("Unable to locate sessions");
1780
1781        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1782
1783        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1784
1785        assert!(!sessions.contains_key(&r_uuid));
1786    }
1787
1788    #[test]
1789    fn test_valueset_oauth2_session_repl_merge_right() {
1790        let s_uuid = Uuid::new_v4();
1791        let r_uuid = Uuid::new_v4();
1792        let zero_cid = Cid::new_zero();
1793        let one_cid = Cid::new_count(1);
1794
1795        let vs_a: ValueSet = ValueSetOauth2Session::new(
1796            s_uuid,
1797            Oauth2Session {
1798                state: SessionState::NeverExpires,
1799                issued_at: OffsetDateTime::now_utc(),
1800                parent: Some(Uuid::new_v4()),
1801                rs_uuid: Uuid::new_v4(),
1802            },
1803        );
1804
1805        let vs_b: ValueSet = ValueSetOauth2Session::from_iter([
1806            (
1807                s_uuid,
1808                Oauth2Session {
1809                    state: SessionState::RevokedAt(one_cid.clone()),
1810                    issued_at: OffsetDateTime::now_utc(),
1811                    parent: Some(Uuid::new_v4()),
1812                    rs_uuid: Uuid::new_v4(),
1813                },
1814            ),
1815            (
1816                r_uuid,
1817                Oauth2Session {
1818                    state: SessionState::RevokedAt(zero_cid.clone()),
1819                    issued_at: OffsetDateTime::now_utc(),
1820                    parent: Some(Uuid::new_v4()),
1821                    rs_uuid: Uuid::new_v4(),
1822                },
1823            ),
1824        ])
1825        .expect("Unable to build valueset oauth2 session");
1826
1827        // Note inverse order
1828        let r_vs = vs_b
1829            .repl_merge_valueset(&vs_a, &one_cid)
1830            .expect("failed to merge");
1831
1832        let sessions = r_vs
1833            .as_oauth2session_map()
1834            .expect("Unable to locate sessions");
1835
1836        let session = sessions.get(&s_uuid).expect("Unable to locate session");
1837
1838        assert_eq!(session.state, SessionState::RevokedAt(one_cid));
1839
1840        assert!(!sessions.contains_key(&r_uuid));
1841    }
1842
1843    #[test]
1844    fn test_valueset_oauth2_session_repl_trim() {
1845        let zero_uuid = Uuid::new_v4();
1846        let zero_cid = Cid::new_zero();
1847        let one_uuid = Uuid::new_v4();
1848        let one_cid = Cid::new_count(1);
1849        let two_uuid = Uuid::new_v4();
1850        let two_cid = Cid::new_count(2);
1851
1852        let mut vs_a: ValueSet = ValueSetOauth2Session::from_iter([
1853            (
1854                zero_uuid,
1855                Oauth2Session {
1856                    state: SessionState::RevokedAt(zero_cid),
1857                    issued_at: OffsetDateTime::now_utc(),
1858                    parent: Some(Uuid::new_v4()),
1859                    rs_uuid: Uuid::new_v4(),
1860                },
1861            ),
1862            (
1863                one_uuid,
1864                Oauth2Session {
1865                    state: SessionState::RevokedAt(one_cid),
1866                    issued_at: OffsetDateTime::now_utc(),
1867                    parent: Some(Uuid::new_v4()),
1868                    rs_uuid: Uuid::new_v4(),
1869                },
1870            ),
1871            (
1872                two_uuid,
1873                Oauth2Session {
1874                    state: SessionState::RevokedAt(two_cid.clone()),
1875                    issued_at: OffsetDateTime::now_utc(),
1876                    parent: Some(Uuid::new_v4()),
1877                    rs_uuid: Uuid::new_v4(),
1878                },
1879            ),
1880        ])
1881        .unwrap();
1882
1883        vs_a.trim(&two_cid);
1884
1885        let sessions = vs_a
1886            .as_oauth2session_map()
1887            .expect("Unable to locate session");
1888
1889        assert!(!sessions.contains_key(&zero_uuid));
1890        assert!(!sessions.contains_key(&one_uuid));
1891        assert!(sessions.contains_key(&two_uuid));
1892    }
1893
1894    #[test]
1895    fn test_scim_session() {
1896        let s_uuid = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
1897
1898        let vs: ValueSet = ValueSetSession::new(
1899            s_uuid,
1900            Session {
1901                label: "hacks".to_string(),
1902                state: SessionState::NeverExpires,
1903                issued_at: OffsetDateTime::UNIX_EPOCH,
1904                issued_by: IdentityId::Internal,
1905                cred_id: s_uuid,
1906                scope: SessionScope::ReadOnly,
1907                type_: AuthType::Passkey,
1908            },
1909        );
1910
1911        let data = r#"
1912[
1913  {
1914    "authType": "passkey",
1915    "credentialId": "3a163ca0-4762-4620-a188-06b750c84c86",
1916    "issuedAt": "1970-01-01T00:00:00Z",
1917    "issuedBy": "00000000-0000-0000-0000-ffffff000000",
1918    "id": "3a163ca0-4762-4620-a188-06b750c84c86",
1919    "sessionScope": "read_only"
1920  }
1921]
1922        "#;
1923        crate::valueset::scim_json_reflexive(&vs, data);
1924    }
1925
1926    #[test]
1927    fn test_scim_oauth2_session() {
1928        let s_uuid = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
1929
1930        let vs: ValueSet = ValueSetOauth2Session::new(
1931            s_uuid,
1932            Oauth2Session {
1933                state: SessionState::NeverExpires,
1934                issued_at: OffsetDateTime::UNIX_EPOCH,
1935                parent: Some(s_uuid),
1936                rs_uuid: s_uuid,
1937            },
1938        );
1939
1940        let data = r#"
1941[
1942  {
1943    "clientId": "3a163ca0-4762-4620-a188-06b750c84c86",
1944    "issuedAt": "1970-01-01T00:00:00Z",
1945    "parentId": "3a163ca0-4762-4620-a188-06b750c84c86",
1946    "id": "3a163ca0-4762-4620-a188-06b750c84c86"
1947  }
1948]
1949        "#;
1950
1951        crate::valueset::scim_json_reflexive(&vs, data);
1952    }
1953}