kanidm/server/lib/src/valueset/session.rs
James Hodgkinson 3eae7be0bb
OAuth2 Token Type ()
* fix(OAuth2): Invalid `token_type` for token introspection
Fixes 

* fix(aut): `assert_eq` instead of `assert ==`

* fix(OAuth2): IANA registry access token types

* fix(OAuth2): deserialize case insensitively
2024-08-25 23:30:20 +00:00

2042 lines
74 KiB
Rust

use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use time::OffsetDateTime;
use crate::be::dbvalue::{
DbCidV1, DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueAuthTypeV1,
DbValueIdentityId, DbValueOauth2Session, DbValueSession, DbValueSessionStateV1,
};
use crate::prelude::*;
use crate::repl::cid::Cid;
use crate::repl::proto::{
ReplApiTokenScopeV1, ReplApiTokenV1, ReplAttrV1, ReplIdentityIdV1, ReplOauth2SessionV1,
ReplSessionStateV1,
};
use crate::schema::SchemaAttribute;
use crate::value::{
ApiToken, ApiTokenScope, AuthType, Oauth2Session, Session, SessionScope, SessionState,
};
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
#[derive(Debug, Clone)]
pub struct ValueSetSession {
map: BTreeMap<Uuid, Session>,
}
impl ValueSetSession {
pub fn new(u: Uuid, m: Session) -> Box<Self> {
let mut map = BTreeMap::new();
map.insert(u, m);
Box::new(ValueSetSession { map })
}
pub fn push(&mut self, u: Uuid, m: Session) -> bool {
self.map.insert(u, m).is_none()
}
fn to_vec_dbvs(&self) -> Vec<DbValueSession> {
self.map
.iter()
.map(|(u, m)| DbValueSession::V4 {
refer: *u,
label: m.label.clone(),
state: match &m.state {
SessionState::ExpiresAt(odt) => {
debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
odt.format(&Rfc3339)
.map(DbValueSessionStateV1::ExpiresAt)
.expect("Failed to format timestamp into RFC3339!")
}
SessionState::NeverExpires => DbValueSessionStateV1::Never,
SessionState::RevokedAt(c) => DbValueSessionStateV1::RevokedAt(DbCidV1 {
server_id: c.s_uuid,
timestamp: c.ts,
}),
},
issued_at: {
debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
m.issued_at
.format(&Rfc3339)
.expect("Failed to format timestamp into RFC3339!")
},
issued_by: match m.issued_by {
IdentityId::Internal => DbValueIdentityId::V1Internal,
IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
},
cred_id: m.cred_id,
scope: match m.scope {
SessionScope::ReadOnly => DbValueAccessScopeV1::ReadOnly,
SessionScope::ReadWrite => DbValueAccessScopeV1::ReadWrite,
SessionScope::PrivilegeCapable => DbValueAccessScopeV1::PrivilegeCapable,
SessionScope::Synchronise => DbValueAccessScopeV1::Synchronise,
},
type_: match m.type_ {
AuthType::Anonymous => DbValueAuthTypeV1::Anonymous,
AuthType::Password => DbValueAuthTypeV1::Password,
AuthType::GeneratedPassword => DbValueAuthTypeV1::GeneratedPassword,
AuthType::PasswordTotp => DbValueAuthTypeV1::PasswordTotp,
AuthType::PasswordBackupCode => DbValueAuthTypeV1::PasswordBackupCode,
AuthType::PasswordSecurityKey => DbValueAuthTypeV1::PasswordSecurityKey,
AuthType::Passkey => DbValueAuthTypeV1::Passkey,
AuthType::AttestedPasskey => DbValueAuthTypeV1::AttestedPasskey,
},
})
.collect()
}
fn from_dbv_iter<'a>(
iter: impl Iterator<Item = &'a DbValueSession>,
) -> Result<ValueSet, OperationError> {
let map = iter
.filter_map(|dbv| {
match dbv {
// We need to ignore all older session records as they lack the AuthType
// record which prevents re-auth working.
DbValueSession::V1 { .. }
| DbValueSession::V2 { .. }
| DbValueSession::V3 { .. } => None,
DbValueSession::V4 {
refer,
label,
state,
issued_at,
issued_by,
cred_id,
scope,
type_,
} => {
// Convert things.
let issued_at = OffsetDateTime::parse(issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
let state = match state {
DbValueSessionStateV1::ExpiresAt(e_inner) => {
OffsetDateTime::parse(e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map(SessionState::ExpiresAt)
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
.ok()?
}
DbValueSessionStateV1::Never => SessionState::NeverExpires,
DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
s_uuid: dc.server_id,
ts: dc.timestamp,
}),
};
let issued_by = match issued_by {
DbValueIdentityId::V1Internal => IdentityId::Internal,
DbValueIdentityId::V1Uuid(u) => IdentityId::User(*u),
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(*u),
};
let scope = match scope {
DbValueAccessScopeV1::IdentityOnly | DbValueAccessScopeV1::ReadOnly => {
SessionScope::ReadOnly
}
DbValueAccessScopeV1::ReadWrite => SessionScope::ReadWrite,
DbValueAccessScopeV1::PrivilegeCapable => {
SessionScope::PrivilegeCapable
}
DbValueAccessScopeV1::Synchronise => SessionScope::Synchronise,
};
let type_ = match type_ {
DbValueAuthTypeV1::Anonymous => AuthType::Anonymous,
DbValueAuthTypeV1::Password => AuthType::Password,
DbValueAuthTypeV1::GeneratedPassword => AuthType::GeneratedPassword,
DbValueAuthTypeV1::PasswordTotp => AuthType::PasswordTotp,
DbValueAuthTypeV1::PasswordBackupCode => AuthType::PasswordBackupCode,
DbValueAuthTypeV1::PasswordSecurityKey => AuthType::PasswordSecurityKey,
DbValueAuthTypeV1::Passkey => AuthType::Passkey,
DbValueAuthTypeV1::AttestedPasskey => AuthType::AttestedPasskey,
};
Some((
*refer,
Session {
label: label.clone(),
state,
issued_at,
issued_by,
cred_id: *cred_id,
scope,
type_,
},
))
}
}
})
.collect();
Ok(Box::new(ValueSetSession { map }))
}
pub fn from_dbvs2(data: &[DbValueSession]) -> Result<ValueSet, OperationError> {
Self::from_dbv_iter(data.iter())
}
pub fn from_repl_v1(data: &[DbValueSession]) -> Result<ValueSet, OperationError> {
Self::from_dbv_iter(data.iter())
}
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
// types, and tuples are always foreign.
#[allow(clippy::should_implement_trait)]
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = (Uuid, Session)>,
{
let map = iter.into_iter().collect();
Some(Box::new(ValueSetSession { map }))
}
}
impl ValueSetT for ValueSetSession {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
Value::Session(u, m) => {
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
e.insert(m);
Ok(true)
} else {
Ok(false)
}
}
_ => Err(OperationError::InvalidValueState),
}
}
fn clear(&mut self) {
self.map.clear();
}
fn remove(&mut self, pv: &PartialValue, cid: &Cid) -> bool {
match pv {
PartialValue::Refer(u) => {
if let Some(session) = self.map.get_mut(u) {
if !matches!(session.state, SessionState::RevokedAt(_)) {
session.state = SessionState::RevokedAt(cid.clone());
true
} else {
false
}
} else {
false
}
}
_ => false,
}
}
fn purge(&mut self, cid: &Cid) -> bool {
for (_uuid, session) in self.map.iter_mut() {
// Send them all to the shadow realm
if !matches!(session.state, SessionState::RevokedAt(_)) {
session.state = SessionState::RevokedAt(cid.clone())
}
}
// Can't be purged since we need the cid's of revoked to persist.
false
}
fn trim(&mut self, trim_cid: &Cid) {
// There might be a neater way to do this with less iterations. The problem
// is we can't just check on what was in b/older, because then we miss
// trimmable content from the local map. So once the merge is complete we
// do a pass for trim.
self.map.retain(|_, session| {
match &session.state {
SessionState::RevokedAt(cid) if cid < trim_cid => {
// This value is past the replication trim window and can now safely
// be removed
false
}
// Retain all else
_ => true,
}
});
// Now, assert that there are fewer or equal sessions to the limit.
if self.map.len() > SESSION_MAXIMUM {
// At this point we will force a number of sessions to be removed. This
// is replication safe since other replicas will also be performing
// the same operation on merge, since we trim by session issuance order.
// This is a "slow path". This is because we optimise session storage
// based on fast session lookup, so now we need to actually create an
// index based on time. We need to also clone here since we need to mutate
// self.map which would violate mut/imut.
warn!(
"entry has exceeded session_maximum limit ({:?}), force trimming will occur",
SESSION_MAXIMUM
);
let time_idx: BTreeMap<OffsetDateTime, Uuid> = self
.map
.iter()
.map(|(session_id, session)| (session.issued_at, *session_id))
.collect();
let to_take = self.map.len() - SESSION_MAXIMUM;
time_idx.values().take(to_take).for_each(|session_id| {
warn!(?session_id, "force trimmed");
self.map.remove(session_id);
});
}
// And we're done.
}
fn contains(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::Refer(u) => self.map.contains_key(u),
_ => false,
}
}
fn substring(&self, _pv: &PartialValue) -> bool {
false
}
fn startswith(&self, _pv: &PartialValue) -> bool {
false
}
fn endswith(&self, _pv: &PartialValue) -> bool {
false
}
fn lessthan(&self, _pv: &PartialValue) -> bool {
false
}
fn len(&self) -> usize {
self.map.len()
}
fn generate_idx_eq_keys(&self) -> Vec<String> {
self.map
.keys()
.map(|u| u.as_hyphenated().to_string())
.collect()
}
fn syntax(&self) -> SyntaxType {
SyntaxType::Session
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
true
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(
self.map
.iter()
.map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
)
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Session(self.to_vec_dbvs())
}
fn to_repl_v1(&self) -> ReplAttrV1 {
ReplAttrV1::Session {
set: self.to_vec_dbvs(),
}
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.map.keys().cloned().map(PartialValue::Refer))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.map.iter().map(|(u, m)| Value::Session(*u, m.clone())))
}
fn equal(&self, other: &ValueSet) -> bool {
if let Some(other) = other.as_session_map() {
&self.map == other
} else {
debug_assert!(false);
false
}
}
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
if let Some(b) = other.as_session_map() {
// We can't just do merge maps here, we have to be aware of the
// session.state value and what it currently is set to.
for (k_other, v_other) in b.iter() {
if let Some(v_self) = self.map.get_mut(k_other) {
// We only update if greater. This is where RevokedAt
// always proceeds other states, and lower revoked
// cids will always take effect.
if v_other.state > v_self.state {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
self.map.insert(*k_other, v_other.clone());
}
}
Ok(())
} else {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
fn as_session_map(&self) -> Option<&BTreeMap<Uuid, Session>> {
Some(&self.map)
}
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
// This is what ties us as a type that can be refint checked.
Some(Box::new(self.map.keys().copied()))
}
fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
// If the older value has a different type - return nothing, we
// just take the newer value.
let b = older.as_session_map()?;
// We can't just do merge maps here, we have to be aware of the
// session.state value and what it currently is set to.
let mut map = self.map.clone();
for (k_other, v_other) in b.iter() {
if let Some(v_self) = map.get_mut(k_other) {
// We only update if lower. This is where RevokedAt
// always proceeds other states, and lower revoked
// cids will always take effect.
if v_other.state > v_self.state {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
map.insert(*k_other, v_other.clone());
}
}
let mut vs = Box::new(ValueSetSession { map });
vs.trim(trim_cid);
Some(vs)
}
}
// == oauth2 session ==
#[derive(Debug, Clone)]
pub struct ValueSetOauth2Session {
map: BTreeMap<Uuid, Oauth2Session>,
// this is a "filter" to tell us if as rs_id is used anywhere
// in this set. The reason is so that we don't do O(n) searches
// on a refer if it's not in this set. The alternate approach is
// an index on these maps, but its more work to maintain for a rare
// situation where we actually want to query rs_uuid -> sessions.
rs_filter: u128,
}
impl ValueSetOauth2Session {
pub fn new(u: Uuid, m: Oauth2Session) -> Box<Self> {
let mut map = BTreeMap::new();
let rs_filter = m.rs_uuid.as_u128();
map.insert(u, m);
Box::new(ValueSetOauth2Session { map, rs_filter })
}
pub fn push(&mut self, u: Uuid, m: Oauth2Session) -> bool {
self.rs_filter |= m.rs_uuid.as_u128();
self.map.insert(u, m).is_none()
}
pub fn from_dbvs2(data: Vec<DbValueOauth2Session>) -> Result<ValueSet, OperationError> {
let mut rs_filter = u128::MIN;
let map = data
.into_iter()
.filter_map(|dbv| {
match dbv {
DbValueOauth2Session::V1 {
refer,
parent,
expiry,
issued_at,
rs_uuid,
} => {
// Convert things.
let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
// This is a bit annoying. In the case we can't parse the optional
// expiry, we need to NOT return the session so that it's immediately
// invalidated. To do this we have to invert some of the options involved
// here.
let expiry = expiry
.map(|e_inner| {
OffsetDateTime::parse(&e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
// We now have an
// Option<Result<ODT, _>>
})
.transpose()
// Result<Option<ODT>, _>
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
// Option<Option<ODT>>
.ok()?;
let state = expiry
.map(SessionState::ExpiresAt)
.unwrap_or(SessionState::NeverExpires);
let parent = Some(parent);
// Insert to the rs_filter.
rs_filter |= rs_uuid.as_u128();
Some((
refer,
Oauth2Session {
parent,
state,
issued_at,
rs_uuid,
},
))
}
DbValueOauth2Session::V2 {
refer,
parent,
state,
issued_at,
rs_uuid,
} => {
// Convert things.
let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
let state = match state {
DbValueSessionStateV1::ExpiresAt(e_inner) => {
OffsetDateTime::parse(&e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map(SessionState::ExpiresAt)
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
.ok()?
}
DbValueSessionStateV1::Never => SessionState::NeverExpires,
DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
s_uuid: dc.server_id,
ts: dc.timestamp,
}),
};
rs_filter |= rs_uuid.as_u128();
let parent = Some(parent);
Some((
refer,
Oauth2Session {
parent,
state,
issued_at,
rs_uuid,
},
))
} // End V2
DbValueOauth2Session::V3 {
refer,
parent,
state,
issued_at,
rs_uuid,
} => {
// Convert things.
let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
let state = match state {
DbValueSessionStateV1::ExpiresAt(e_inner) => {
OffsetDateTime::parse(&e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map(SessionState::ExpiresAt)
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
.ok()?
}
DbValueSessionStateV1::Never => SessionState::NeverExpires,
DbValueSessionStateV1::RevokedAt(dc) => SessionState::RevokedAt(Cid {
s_uuid: dc.server_id,
ts: dc.timestamp,
}),
};
rs_filter |= rs_uuid.as_u128();
Some((
refer,
Oauth2Session {
parent,
state,
issued_at,
rs_uuid,
},
))
} // End V3
}
})
.collect();
Ok(Box::new(ValueSetOauth2Session { map, rs_filter }))
}
pub fn from_repl_v1(data: &[ReplOauth2SessionV1]) -> Result<ValueSet, OperationError> {
let mut rs_filter = u128::MIN;
let map = data
.iter()
.filter_map(
|ReplOauth2SessionV1 {
refer,
parent,
state,
issued_at,
rs_uuid,
}| {
// Convert things.
let issued_at = OffsetDateTime::parse(issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
// This is a bit annoying. In the case we can't parse the optional
// expiry, we need to NOT return the session so that it's immediately
// invalidated. To do this we have to invert some of the options involved
// here.
let state = match state {
ReplSessionStateV1::ExpiresAt(e_inner) => {
OffsetDateTime::parse(e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map(SessionState::ExpiresAt)
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
.ok()?
}
ReplSessionStateV1::Never => SessionState::NeverExpires,
ReplSessionStateV1::RevokedAt(rc) => SessionState::RevokedAt(rc.into()),
};
rs_filter |= rs_uuid.as_u128();
Some((
*refer,
Oauth2Session {
parent: *parent,
state,
issued_at,
rs_uuid: *rs_uuid,
},
))
},
)
.collect();
Ok(Box::new(ValueSetOauth2Session { rs_filter, map }))
}
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
// types, and tuples are always foreign.
#[allow(clippy::should_implement_trait)]
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = (Uuid, Oauth2Session)>,
{
let mut rs_filter = u128::MIN;
let map = iter
.into_iter()
.map(|(u, m)| {
rs_filter |= m.rs_uuid.as_u128();
(u, m)
})
.collect();
Some(Box::new(ValueSetOauth2Session { map, rs_filter }))
}
}
impl ValueSetT for ValueSetOauth2Session {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
Value::Oauth2Session(u, m) => {
// Unlike other types, this allows overwriting as oauth2 sessions
// can be *extended* in time length.
match self.map.entry(u) {
BTreeEntry::Vacant(e) => {
self.rs_filter |= m.rs_uuid.as_u128();
e.insert(m);
Ok(true)
}
BTreeEntry::Occupied(mut e) => {
let e_v = e.get_mut();
if m.state > e_v.state {
// Replace if the state has higher priority.
*e_v = m;
Ok(true)
} else {
// Else take no action.
Ok(false)
}
}
}
}
_ => Err(OperationError::InvalidValueState),
}
}
fn clear(&mut self) {
self.rs_filter = u128::MIN;
self.map.clear();
}
fn remove(&mut self, pv: &PartialValue, cid: &Cid) -> bool {
match pv {
PartialValue::Refer(u) => {
if let Some(session) = self.map.get_mut(u) {
if !matches!(session.state, SessionState::RevokedAt(_)) {
session.state = SessionState::RevokedAt(cid.clone());
true
} else {
false
}
} else {
// What if it's an rs_uuid?
let u_int = u.as_u128();
if self.rs_filter & u_int == u_int {
// It's there, so we need to do a more costly revoke over the values
// that are present.
let mut removed = false;
self.map.values_mut().for_each(|session| {
if session.rs_uuid == *u {
session.state = SessionState::RevokedAt(cid.clone());
removed = true;
}
});
removed
} else {
// It's not in the rs_filter or the map, false.
false
}
}
}
_ => false,
}
}
fn purge(&mut self, cid: &Cid) -> bool {
for (_uuid, session) in self.map.iter_mut() {
// Send them all to the shadow realm
if !matches!(session.state, SessionState::RevokedAt(_)) {
session.state = SessionState::RevokedAt(cid.clone())
}
}
// Can't be purged since we need the cid's of revoked to persist.
false
}
fn trim(&mut self, trim_cid: &Cid) {
// There might be a neater way to do this with less iterations. The problem
// is we can't just check on what was in b/older, because then we miss
// trimmable content from the local map. So once the merge is complete we
// do a pass for trim.
self.map.retain(|_, session| {
match &session.state {
SessionState::RevokedAt(cid) if cid < trim_cid => {
// This value is past the replication trim window and can now safely
// be removed
false
}
// Retain all else
_ => true,
}
})
}
fn contains(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::Refer(u) => {
self.map.contains_key(u) || {
let u_int = u.as_u128();
if self.rs_filter & u_int == u_int {
self.map.values().any(|session| {
session.rs_uuid == *u
&& !matches!(session.state, SessionState::RevokedAt(_))
})
} else {
false
}
}
}
_ => false,
}
}
fn substring(&self, _pv: &PartialValue) -> bool {
false
}
fn startswith(&self, _pv: &PartialValue) -> bool {
false
}
fn endswith(&self, _pv: &PartialValue) -> bool {
false
}
fn lessthan(&self, _pv: &PartialValue) -> bool {
false
}
fn len(&self) -> usize {
self.map.len()
}
fn generate_idx_eq_keys(&self) -> Vec<String> {
// Allocate twice as much for worst-case when every session is
// a unique rs-uuid to prevent re-allocs.
let mut idx_keys = Vec::with_capacity(self.map.len() * 2);
for (k, v) in self.map.iter() {
idx_keys.push(k.as_hyphenated().to_string());
idx_keys.push(v.rs_uuid.as_hyphenated().to_string());
}
idx_keys.sort_unstable();
idx_keys.dedup();
idx_keys
}
fn syntax(&self) -> SyntaxType {
SyntaxType::Oauth2Session
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
true
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(
self.map
.iter()
.map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
)
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::Oauth2Session(
self.map
.iter()
.map(|(u, m)| DbValueOauth2Session::V3 {
refer: *u,
parent: m.parent,
state: match &m.state {
SessionState::ExpiresAt(odt) => {
debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
odt.format(&Rfc3339)
.map(DbValueSessionStateV1::ExpiresAt)
.expect("Failed to format timestamp into RFC3339!")
}
SessionState::NeverExpires => DbValueSessionStateV1::Never,
SessionState::RevokedAt(c) => DbValueSessionStateV1::RevokedAt(DbCidV1 {
server_id: c.s_uuid,
timestamp: c.ts,
}),
},
issued_at: {
debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
m.issued_at
.format(&Rfc3339)
.expect("Failed to format timestamp as RFC3339")
},
rs_uuid: m.rs_uuid,
})
.collect(),
)
}
fn to_repl_v1(&self) -> ReplAttrV1 {
ReplAttrV1::Oauth2Session {
set: self
.map
.iter()
.map(|(u, m)| ReplOauth2SessionV1 {
refer: *u,
parent: m.parent,
state: match &m.state {
SessionState::ExpiresAt(odt) => {
debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
odt.format(&Rfc3339)
.map(ReplSessionStateV1::ExpiresAt)
.expect("Failed to format timestamp into RFC3339!")
}
SessionState::NeverExpires => ReplSessionStateV1::Never,
SessionState::RevokedAt(c) => ReplSessionStateV1::RevokedAt(c.into()),
},
issued_at: {
debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
m.issued_at
.format(&Rfc3339)
.expect("Failed to format timestamp into RFC3339")
},
rs_uuid: m.rs_uuid,
})
.collect(),
}
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.map.keys().cloned().map(PartialValue::Refer))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(
self.map
.iter()
.map(|(u, m)| Value::Oauth2Session(*u, m.clone())),
)
}
fn equal(&self, other: &ValueSet) -> bool {
if let Some(other) = other.as_oauth2session_map() {
&self.map == other
} else {
debug_assert!(false);
false
}
}
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
if let Some(b) = other.as_oauth2session_map() {
// We can't just do merge maps here, we have to be aware of the
// session.state value and what it currently is set to. We also
// need to make sure the rs_filter is updated too!
for (k_other, v_other) in b.iter() {
if let Some(v_self) = self.map.get_mut(k_other) {
// We only update if lower. This is where RevokedAt
// always proceeds other states, and lower revoked
// cids will always take effect.
if v_other.state > v_self.state {
*v_self = v_other.clone();
}
} else {
// Update the rs_filter!
self.rs_filter |= v_other.rs_uuid.as_u128();
// Not present, just insert.
self.map.insert(*k_other, v_other.clone());
}
}
Ok(())
} else {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
fn as_oauth2session_map(&self) -> Option<&BTreeMap<Uuid, Oauth2Session>> {
Some(&self.map)
}
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
// This is what ties us as a type that can be refint checked. We need to
// bind to our resource servers, not our ids!
Some(Box::new(self.map.values().map(|m| &m.rs_uuid).copied()))
}
fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
if let Some(b) = older.as_oauth2session_map() {
// We can't just do merge maps here, we have to be aware of the
// session.state value and what it currently is set to.
let mut map = self.map.clone();
let mut rs_filter = self.rs_filter;
for (k_other, v_other) in b.iter() {
if let Some(v_self) = map.get_mut(k_other) {
// We only update if greater. This is where RevokedAt
// always proceeds other states, and lower revoked
// cids will always take effect.
if v_other.state > v_self.state {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
rs_filter |= v_other.rs_uuid.as_u128();
map.insert(*k_other, v_other.clone());
}
}
let mut vs = Box::new(ValueSetOauth2Session { map, rs_filter });
vs.trim(trim_cid);
Some(vs)
} else {
// The older value has a different type - return nothing, we
// just take the newer value.
None
}
}
}
#[derive(Debug, Clone)]
pub struct ValueSetApiToken {
map: BTreeMap<Uuid, ApiToken>,
}
impl ValueSetApiToken {
pub fn new(u: Uuid, m: ApiToken) -> Box<Self> {
let mut map = BTreeMap::new();
map.insert(u, m);
Box::new(ValueSetApiToken { map })
}
pub fn push(&mut self, u: Uuid, m: ApiToken) -> bool {
self.map.insert(u, m).is_none()
}
pub fn from_dbvs2(data: Vec<DbValueApiToken>) -> Result<ValueSet, OperationError> {
let map = data
.into_iter()
.filter_map(|dbv| {
match dbv {
DbValueApiToken::V1 {
refer,
label,
expiry,
issued_at,
issued_by,
scope,
} => {
// Convert things.
let issued_at = OffsetDateTime::parse(&issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating api token {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
// This is a bit annoying. In the case we can't parse the optional
// expiry, we need to NOT return the session so that it's immediately
// invalidated. To do this we have to invert some of the options involved
// here.
let expiry = expiry
.map(|e_inner| {
OffsetDateTime::parse(&e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
// We now have an
// Option<Result<ODT, _>>
})
.transpose()
// Result<Option<ODT>, _>
.map_err(|e| {
admin_error!(
?e,
"Invalidating api token {} due to invalid expiry timestamp",
refer
)
})
// Option<Option<ODT>>
.ok()?;
let issued_by = match issued_by {
DbValueIdentityId::V1Internal => IdentityId::Internal,
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
};
let scope = match scope {
DbValueApiTokenScopeV1::ReadOnly => ApiTokenScope::ReadOnly,
DbValueApiTokenScopeV1::ReadWrite => ApiTokenScope::ReadWrite,
DbValueApiTokenScopeV1::Synchronise => ApiTokenScope::Synchronise,
};
Some((
refer,
ApiToken {
label,
expiry,
issued_at,
issued_by,
scope,
},
))
}
}
})
.collect();
Ok(Box::new(ValueSetApiToken { map }))
}
pub fn from_repl_v1(data: &[ReplApiTokenV1]) -> Result<ValueSet, OperationError> {
let map = data
.iter()
.filter_map(
|ReplApiTokenV1 {
refer,
label,
expiry,
issued_at,
issued_by,
scope,
}| {
// Convert things.
let issued_at = OffsetDateTime::parse(issued_at, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid issued_at timestamp",
refer
)
})
.ok()?;
// This is a bit annoying. In the case we can't parse the optional
// expiry, we need to NOT return the session so that it's immediately
// invalidated. To do this we have to invert some of the options involved
// here.
let expiry = expiry
.as_ref()
.map(|e_inner| {
OffsetDateTime::parse(e_inner, &Rfc3339)
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
// We now have an
// Option<Result<ODT, _>>
})
.transpose()
// Result<Option<ODT>, _>
.map_err(|e| {
admin_error!(
?e,
"Invalidating session {} due to invalid expiry timestamp",
refer
)
})
// Option<Option<ODT>>
.ok()?;
let issued_by = match issued_by {
ReplIdentityIdV1::Internal => IdentityId::Internal,
ReplIdentityIdV1::Uuid(u) => IdentityId::User(*u),
ReplIdentityIdV1::Synch(u) => IdentityId::Synch(*u),
};
let scope = match scope {
ReplApiTokenScopeV1::ReadOnly => ApiTokenScope::ReadOnly,
ReplApiTokenScopeV1::ReadWrite => ApiTokenScope::ReadWrite,
ReplApiTokenScopeV1::Synchronise => ApiTokenScope::Synchronise,
};
Some((
*refer,
ApiToken {
label: label.to_string(),
expiry,
issued_at,
issued_by,
scope,
},
))
},
)
.collect();
Ok(Box::new(ValueSetApiToken { map }))
}
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
// types, and tuples are always foreign.
#[allow(clippy::should_implement_trait)]
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = (Uuid, ApiToken)>,
{
let map = iter.into_iter().collect();
Some(Box::new(ValueSetApiToken { map }))
}
}
impl ValueSetT for ValueSetApiToken {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
Value::ApiToken(u, m) => {
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
e.insert(m);
Ok(true)
} else {
Ok(false)
}
}
_ => Err(OperationError::InvalidValueState),
}
}
fn clear(&mut self) {
self.map.clear();
}
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
match pv {
PartialValue::Refer(u) => self.map.remove(u).is_some(),
_ => false,
}
}
fn purge(&mut self, _cid: &Cid) -> bool {
// Could consider making this a TS capable entry.
true
}
fn contains(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::Refer(u) => self.map.contains_key(u),
_ => false,
}
}
fn substring(&self, _pv: &PartialValue) -> bool {
false
}
fn startswith(&self, _pv: &PartialValue) -> bool {
false
}
fn endswith(&self, _pv: &PartialValue) -> bool {
false
}
fn lessthan(&self, _pv: &PartialValue) -> bool {
false
}
fn len(&self) -> usize {
self.map.len()
}
fn generate_idx_eq_keys(&self) -> Vec<String> {
self.map
.keys()
.map(|u| u.as_hyphenated().to_string())
.collect()
}
fn syntax(&self) -> SyntaxType {
SyntaxType::ApiToken
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
self.map.iter().all(|(_, at)| {
Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label)
})
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(
self.map
.iter()
.map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
)
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::ApiToken(
self.map
.iter()
.map(|(u, m)| DbValueApiToken::V1 {
refer: *u,
label: m.label.clone(),
expiry: m.expiry.map(|odt| {
debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
odt.format(&Rfc3339)
.expect("Failed to format timestamp into RFC3339")
}),
issued_at: {
debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
m.issued_at
.format(&Rfc3339)
.expect("Failed to format timestamp into RFC3339")
},
issued_by: match m.issued_by {
IdentityId::Internal => DbValueIdentityId::V1Internal,
IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
},
scope: match m.scope {
ApiTokenScope::ReadOnly => DbValueApiTokenScopeV1::ReadOnly,
ApiTokenScope::ReadWrite => DbValueApiTokenScopeV1::ReadWrite,
ApiTokenScope::Synchronise => DbValueApiTokenScopeV1::Synchronise,
},
})
.collect(),
)
}
fn to_repl_v1(&self) -> ReplAttrV1 {
ReplAttrV1::ApiToken {
set: self
.map
.iter()
.map(|(u, m)| ReplApiTokenV1 {
refer: *u,
label: m.label.clone(),
expiry: m.expiry.map(|odt| {
debug_assert_eq!(odt.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
odt.format(&Rfc3339)
.expect("Failed to format timestamp into RFC3339")
}),
issued_at: {
debug_assert_eq!(m.issued_at.offset(), time::UtcOffset::UTC);
#[allow(clippy::expect_used)]
m.issued_at
.format(&Rfc3339)
.expect("Failed to format timestamp into RFC3339")
},
issued_by: match m.issued_by {
IdentityId::Internal => ReplIdentityIdV1::Internal,
IdentityId::User(u) => ReplIdentityIdV1::Uuid(u),
IdentityId::Synch(u) => ReplIdentityIdV1::Synch(u),
},
scope: match m.scope {
ApiTokenScope::ReadOnly => ReplApiTokenScopeV1::ReadOnly,
ApiTokenScope::ReadWrite => ReplApiTokenScopeV1::ReadWrite,
ApiTokenScope::Synchronise => ReplApiTokenScopeV1::Synchronise,
},
})
.collect(),
}
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.map.keys().cloned().map(PartialValue::Refer))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.map.iter().map(|(u, m)| Value::ApiToken(*u, m.clone())))
}
fn equal(&self, other: &ValueSet) -> bool {
if let Some(other) = other.as_apitoken_map() {
&self.map == other
} else {
debug_assert!(false);
false
}
}
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
if let Some(b) = other.as_apitoken_map() {
mergemaps!(self.map, b)
} else {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
fn as_apitoken_map(&self) -> Option<&BTreeMap<Uuid, ApiToken>> {
Some(&self.map)
}
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
// This is what ties us as a type that can be refint checked.
Some(Box::new(self.map.keys().copied()))
}
}
#[cfg(test)]
mod tests {
use super::{ValueSetOauth2Session, ValueSetSession, SESSION_MAXIMUM};
use crate::prelude::{IdentityId, SessionScope, Uuid};
use crate::repl::cid::Cid;
use crate::value::{AuthType, Oauth2Session, Session, SessionState};
use crate::valueset::ValueSet;
use time::OffsetDateTime;
#[test]
fn test_valueset_session_purge() {
let s_uuid = Uuid::new_v4();
let mut vs: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
let zero_cid = Cid::new_zero();
// Simulate session revocation.
vs.purge(&zero_cid);
assert_eq!(vs.len(), 1);
let session = vs
.as_session_map()
.and_then(|map| map.get(&s_uuid))
.expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
}
#[test]
fn test_valueset_session_merge_left() {
let s_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let mut vs_a: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
let vs_b: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
vs_a.merge(&vs_b).expect("failed to merge");
let session = vs_a
.as_session_map()
.and_then(|map| map.get(&s_uuid))
.expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
}
#[test]
fn test_valueset_session_merge_right() {
let s_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let vs_a: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
let mut vs_b: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
// Note - inverse order!
vs_b.merge(&vs_a).expect("failed to merge");
let session = vs_b
.as_session_map()
.and_then(|map| map.get(&s_uuid))
.expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
}
#[test]
fn test_valueset_session_repl_merge_left() {
let s_uuid = Uuid::new_v4();
let r_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let one_cid = Cid::new_count(1);
let vs_a: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
let vs_b: ValueSet = ValueSetSession::from_iter([
(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::RevokedAt(one_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
(
r_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
])
.expect("Unable to build valueset session");
let r_vs = vs_a
.repl_merge_valueset(&vs_b, &one_cid)
.expect("failed to merge");
let sessions = r_vs.as_session_map().expect("Unable to locate sessions");
let session = sessions.get(&s_uuid).expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(one_cid));
assert!(!sessions.contains_key(&r_uuid));
}
#[test]
fn test_valueset_session_repl_merge_right() {
let s_uuid = Uuid::new_v4();
let r_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let one_cid = Cid::new_count(1);
let vs_a: ValueSet = ValueSetSession::new(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
);
let vs_b: ValueSet = ValueSetSession::from_iter([
(
s_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::RevokedAt(one_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
(
r_uuid,
Session {
label: "hacks".to_string(),
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
])
.expect("Unable to build valueset session");
// Note - inverse order!
let r_vs = vs_b
.repl_merge_valueset(&vs_a, &one_cid)
.expect("failed to merge");
let sessions = r_vs.as_session_map().expect("Unable to locate sessions");
let session = sessions.get(&s_uuid).expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(one_cid));
assert!(!sessions.contains_key(&r_uuid));
}
#[test]
fn test_valueset_session_repl_trim() {
let zero_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let one_uuid = Uuid::new_v4();
let one_cid = Cid::new_count(1);
let two_uuid = Uuid::new_v4();
let two_cid = Cid::new_count(2);
let mut vs_a: ValueSet = ValueSetSession::from_iter([
(
zero_uuid,
Session {
state: SessionState::RevokedAt(zero_cid),
label: "hacks".to_string(),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
(
one_uuid,
Session {
state: SessionState::RevokedAt(one_cid),
label: "hacks".to_string(),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
(
two_uuid,
Session {
state: SessionState::RevokedAt(two_cid.clone()),
label: "hacks".to_string(),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
),
])
.unwrap();
vs_a.trim(&two_cid);
let sessions = vs_a.as_session_map().expect("Unable to locate session");
assert!(!sessions.contains_key(&zero_uuid));
assert!(!sessions.contains_key(&one_uuid));
assert!(sessions.contains_key(&two_uuid));
}
#[test]
fn test_valueset_session_limit_trim() {
// Create a session that will be trimmed.
let zero_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let issued_at = OffsetDateTime::UNIX_EPOCH;
let session_iter = std::iter::once((
zero_uuid,
Session {
state: SessionState::NeverExpires,
label: "hacks".to_string(),
issued_at,
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
))
.chain((0..SESSION_MAXIMUM).map(|_| {
(
Uuid::new_v4(),
Session {
state: SessionState::NeverExpires,
label: "hacks".to_string(),
issued_at: OffsetDateTime::now_utc(),
issued_by: IdentityId::Internal,
cred_id: Uuid::new_v4(),
scope: SessionScope::ReadOnly,
type_: AuthType::Passkey,
},
)
}));
let mut vs_a: ValueSet = ValueSetSession::from_iter(session_iter).unwrap();
assert!(vs_a.len() > SESSION_MAXIMUM);
vs_a.trim(&zero_cid);
assert_eq!(vs_a.len(), SESSION_MAXIMUM);
let sessions = vs_a.as_session_map().expect("Unable to access sessions");
assert!(!sessions.contains_key(&zero_uuid));
}
#[test]
fn test_valueset_oauth2_session_purge() {
let s_uuid = Uuid::new_v4();
let mut vs: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
let zero_cid = Cid::new_zero();
// Simulate session revocation.
vs.purge(&zero_cid);
assert_eq!(vs.len(), 1);
let session = vs
.as_oauth2session_map()
.and_then(|map| map.get(&s_uuid))
.expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
}
#[test]
fn test_valueset_oauth2_session_merge_left() {
let s_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let mut vs_a: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
let vs_b: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
vs_a.merge(&vs_b).expect("failed to merge");
let session = vs_a
.as_oauth2session_map()
.and_then(|map| map.get(&s_uuid))
.expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
}
#[test]
fn test_valueset_oauth2_session_merge_right() {
let s_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let vs_a: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
let mut vs_b: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
// Note inverse order
vs_b.merge(&vs_a).expect("failed to merge");
let session = vs_b
.as_oauth2session_map()
.and_then(|map| map.get(&s_uuid))
.expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(zero_cid));
}
#[test]
fn test_valueset_oauth2_session_repl_merge_left() {
let s_uuid = Uuid::new_v4();
let r_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let one_cid = Cid::new_count(1);
let vs_a: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
let vs_b: ValueSet = ValueSetOauth2Session::from_iter([
(
s_uuid,
Oauth2Session {
state: SessionState::RevokedAt(one_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
(
r_uuid,
Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
])
.expect("Unable to build valueset oauth2 session");
let r_vs = vs_a
.repl_merge_valueset(&vs_b, &one_cid)
.expect("failed to merge");
let sessions = r_vs
.as_oauth2session_map()
.expect("Unable to locate sessions");
let session = sessions.get(&s_uuid).expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(one_cid));
assert!(!sessions.contains_key(&r_uuid));
}
#[test]
fn test_valueset_oauth2_session_repl_merge_right() {
let s_uuid = Uuid::new_v4();
let r_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let one_cid = Cid::new_count(1);
let vs_a: ValueSet = ValueSetOauth2Session::new(
s_uuid,
Oauth2Session {
state: SessionState::NeverExpires,
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
);
let vs_b: ValueSet = ValueSetOauth2Session::from_iter([
(
s_uuid,
Oauth2Session {
state: SessionState::RevokedAt(one_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
(
r_uuid,
Oauth2Session {
state: SessionState::RevokedAt(zero_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
])
.expect("Unable to build valueset oauth2 session");
// Note inverse order
let r_vs = vs_b
.repl_merge_valueset(&vs_a, &one_cid)
.expect("failed to merge");
let sessions = r_vs
.as_oauth2session_map()
.expect("Unable to locate sessions");
let session = sessions.get(&s_uuid).expect("Unable to locate session");
assert_eq!(session.state, SessionState::RevokedAt(one_cid));
assert!(!sessions.contains_key(&r_uuid));
}
#[test]
fn test_valueset_oauth2_session_repl_trim() {
let zero_uuid = Uuid::new_v4();
let zero_cid = Cid::new_zero();
let one_uuid = Uuid::new_v4();
let one_cid = Cid::new_count(1);
let two_uuid = Uuid::new_v4();
let two_cid = Cid::new_count(2);
let mut vs_a: ValueSet = ValueSetOauth2Session::from_iter([
(
zero_uuid,
Oauth2Session {
state: SessionState::RevokedAt(zero_cid),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
(
one_uuid,
Oauth2Session {
state: SessionState::RevokedAt(one_cid),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
(
two_uuid,
Oauth2Session {
state: SessionState::RevokedAt(two_cid.clone()),
issued_at: OffsetDateTime::now_utc(),
parent: Some(Uuid::new_v4()),
rs_uuid: Uuid::new_v4(),
},
),
])
.unwrap();
vs_a.trim(&two_cid);
let sessions = vs_a
.as_oauth2session_map()
.expect("Unable to locate session");
assert!(!sessions.contains_key(&zero_uuid));
assert!(!sessions.contains_key(&one_uuid));
assert!(sessions.contains_key(&two_uuid));
}
}