kanidmd_lib/valueset/
session.rs

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