kanidm/server/lib/src/plugins/session.rs
2024-03-09 16:09:15 +10:00

891 lines
34 KiB
Rust

//! This plugin maintains consistency of authenticated sessions on accounts.
//!
//! An example of this is that oauth2 sessions are child of user auth sessions,
//! such than when the user auth session is terminated, then the corresponding
//! oauth2 session should also be terminated.
//!
//! This plugin is also responsible for invaliding old sessions that are past
//! their expiry.
use crate::event::ModifyEvent;
use crate::plugins::Plugin;
use crate::prelude::*;
use crate::value::SessionState;
use std::collections::BTreeSet;
use std::sync::Arc;
use time::OffsetDateTime;
pub struct SessionConsistency {}
impl Plugin for SessionConsistency {
fn id() -> &'static str {
"plugin_session_consistency"
}
#[instrument(level = "debug", name = "session_consistency", skip_all)]
fn pre_modify(
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &ModifyEvent,
) -> Result<(), OperationError> {
Self::modify_inner(qs, cand)
}
#[instrument(level = "debug", name = "session_consistency", skip_all)]
fn pre_batch_modify(
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &BatchModifyEvent,
) -> Result<(), OperationError> {
Self::modify_inner(qs, cand)
}
}
impl SessionConsistency {
fn modify_inner<T: Clone + std::fmt::Debug>(
qs: &mut QueryServerWriteTransaction,
cand: &mut [Entry<EntryInvalid, T>],
) -> Result<(), OperationError> {
let curtime = qs.get_curtime();
let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
trace!(%curtime_odt);
// We need to assert a number of properties. We must do these *in order*.
cand.iter_mut().try_for_each(|entry| {
// * If the session's credential is no longer on the account, we remove the session.
let cred_ids: BTreeSet<Uuid> =
entry
.get_ava_single_credential(Attribute::PrimaryCredential)
.iter()
.map(|c| c.uuid)
.chain(
entry.get_ava_passkeys(Attribute::PassKeys)
.iter()
.flat_map(|pks| pks.keys().copied())
)
.chain(
entry.get_ava_attestedpasskeys(Attribute::AttestedPasskeys)
.iter()
.flat_map(|pks| pks.keys().copied())
)
.collect();
let invalidate: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.map(|sessions| {
sessions.iter().filter_map(|(session_id, session)| {
match &session.state {
SessionState::RevokedAt(_) => {
// Ignore, it's already revoked.
None
}
SessionState::ExpiresAt(_) |
SessionState::NeverExpires =>
if !cred_ids.contains(&session.cred_id) {
info!(%session_id, "Revoking auth session whose issuing credential no longer exists");
Some(PartialValue::Refer(*session_id))
} else {
None
},
}
})
.collect()
});
if let Some(invalidate) = invalidate.as_ref() {
entry.remove_avas(Attribute::UserAuthTokenSession, invalidate);
}
// * If a UAT is past its expiry, remove it.
let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.map(|sessions| {
sessions.iter().filter_map(|(session_id, session)| {
trace!(?session_id, ?session);
match &session.state {
SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
info!(%session_id, "Removing expired auth session");
Some(PartialValue::Refer(*session_id))
}
_ => None,
}
})
.collect()
});
if let Some(expired) = expired.as_ref() {
entry.remove_avas(Attribute::UserAuthTokenSession, expired);
}
// * If an oauth2 session is past it's expiry, remove it.
// * If an oauth2 session is past the grace window, and no parent session exists, remove it.
let oauth2_remove: Option<BTreeSet<_>> = entry.get_ava_as_oauth2session_map(Attribute::OAuth2Session).map(|oauth2_sessions| {
// If we have oauth2 sessions, we need to be able to lookup if sessions exist in the uat.
let sessions = entry.get_ava_as_session_map(Attribute::UserAuthTokenSession);
oauth2_sessions.iter().filter_map(|(o2_session_id, session)| {
trace!(?o2_session_id, ?session);
match &session.state {
SessionState::ExpiresAt(exp) if exp <= &curtime_odt => {
info!(%o2_session_id, "Removing expired oauth2 session");
Some(PartialValue::Refer(*o2_session_id))
}
SessionState::RevokedAt(_) => {
// no-op, it's already revoked.
trace!("Skip already revoked session");
None
}
_ => {
// Okay, now check the issued / grace time for parent enforcement.
if sessions.map(|session_map| {
if let Some(parent_session_id) = session.parent.as_ref() {
// A parent session id exists - validate it exists in the account.
if let Some(parent_session) = session_map.get(parent_session_id) {
// Only match non-revoked sessions
!matches!(parent_session.state, SessionState::RevokedAt(_))
} else {
// not found
false
}
} else {
// The session specifically has no parent session and so is
// not bounded by it's presence.
true
}
}).unwrap_or(false) {
// The parent exists and is still valid, go ahead
debug!("Parent session remains valid.");
None
} else {
// Can't find the parent. Are we within grace window
if session.issued_at + GRACE_WINDOW <= curtime_odt {
info!(%o2_session_id, parent_id = ?session.parent, "Removing orphaned oauth2 session");
Some(PartialValue::Refer(*o2_session_id))
} else {
// Grace window is still in effect
debug!("Not enforcing parent session consistency on session within grace window");
None
}
}
}
}
})
.collect()
});
if let Some(oauth2_remove) = oauth2_remove.as_ref() {
entry.remove_avas(Attribute::OAuth2Session, oauth2_remove);
}
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::event::CreateEvent;
use crate::value::{Oauth2Session, Session, SessionState};
use kanidm_proto::constants::OAUTH2_SCOPE_OPENID;
use std::time::Duration;
use time::OffsetDateTime;
use uuid::uuid;
use crate::credential::Credential;
use kanidm_lib_crypto::CryptoPolicy;
// Test expiry of old sessions
#[qs_test]
async fn test_session_consistency_expire_old_sessions(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
let cred_id = cred.uuid;
let exp_curtime = curtime + Duration::from_secs(60);
let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
// Create a user
let mut server_txn = server.write(curtime).await;
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(tuuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1")),
(
Attribute::PrimaryCredential,
Value::Cred("primary".to_string(), cred.clone())
)
);
let ce = CreateEvent::new_internal(vec![e1]);
assert!(server_txn.create(&ce).is_ok());
// Create a fake session.
let session_id = Uuid::new_v4();
let state = SessionState::ExpiresAt(exp_curtime_odt);
let issued_at = curtime_odt;
let issued_by = IdentityId::User(tuuid);
let scope = SessionScope::ReadOnly;
let session = Value::Session(
session_id,
Session {
label: "label".to_string(),
state,
// Need the other inner bits?
// for the gracewindow.
issued_at,
// Who actually created this?
issued_by,
cred_id,
// What is the access scope of this session? This is
// for auditing purposes.
scope,
},
);
// Mod the user
let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Still there
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::ExpiresAt(_)));
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(exp_curtime).await;
// Mod again - anything will do.
let modlist = ModifyList::new_purge_and_set(
Attribute::Description,
Value::new_utf8s("test person 1 change"),
);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Session gone.
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
// We get the attribute and have to check it's now in a revoked state.
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::RevokedAt(_)));
assert!(server_txn.commit().is_ok());
}
// Test expiry of old oauth2 sessions
#[qs_test]
async fn test_session_consistency_oauth2_expiry_cleanup(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
let cred_id = cred.uuid;
// Set exp to gracewindow.
let exp_curtime = curtime + GRACE_WINDOW;
let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
// Create a user
let mut server_txn = server.write(curtime).await;
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
let rs_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(tuuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1")),
(
Attribute::PrimaryCredential,
Value::Cred("primary".to_string(), cred.clone())
)
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(
Attribute::Class,
EntryClass::OAuth2ResourceServerBasic.to_value()
),
(Attribute::Uuid, Value::Uuid(rs_uuid)),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("https://demo.example.com").unwrap()
),
// System admins
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset![OAUTH2_SCOPE_OPENID.to_string()]
)
.expect("invalid oauthscope")
)
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(server_txn.create(&ce).is_ok());
// Create a fake session and oauth2 session.
let session_id = Uuid::new_v4();
let parent_id = Uuid::new_v4();
let state = SessionState::ExpiresAt(exp_curtime_odt);
let issued_at = curtime_odt;
let issued_by = IdentityId::User(tuuid);
let scope = SessionScope::ReadOnly;
// Mod the user
let modlist = modlist!([
Modify::Present(
"oauth2_session".into(),
Value::Oauth2Session(
session_id,
Oauth2Session {
parent: Some(parent_id),
// Set to the exp window.
state,
issued_at,
rs_uuid,
},
)
),
Modify::Present(
Attribute::UserAuthTokenSession.into(),
Value::Session(
parent_id,
Session {
label: "label".to_string(),
// Note we set the exp to None so we are not removing based on removal of the parent.
state: SessionState::NeverExpires,
// Need the other inner bits?
// for the gracewindow.
issued_at,
// Who actually created this?
issued_by,
cred_id,
// What is the access scope of this session? This is
// for auditing purposes.
scope,
},
)
),
]);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Still there
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&parent_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::NeverExpires));
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::ExpiresAt(_)));
assert!(server_txn.commit().is_ok());
// Note as we are now past exp time, the oauth2 session will be removed, but the uat session
// will remain.
let mut server_txn = server.write(exp_curtime).await;
// Mod again - anything will do.
let modlist = ModifyList::new_purge_and_set(
Attribute::Description,
Value::new_utf8s("test person 1 change"),
);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Session gone.
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
// Note the uat is still present
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&parent_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::NeverExpires));
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::RevokedAt(_)));
assert!(server_txn.commit().is_ok());
}
// test removal of a session removes related oauth2 sessions.
#[qs_test]
async fn test_session_consistency_oauth2_removed_by_parent(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
let exp_curtime = curtime + GRACE_WINDOW;
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
let cred_id = cred.uuid;
// Create a user
let mut server_txn = server.write(curtime).await;
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
let rs_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(tuuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1")),
(
Attribute::PrimaryCredential,
Value::Cred("primary".to_string(), cred.clone())
)
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(
Attribute::Class,
EntryClass::OAuth2ResourceServerBasic.to_value()
),
(Attribute::Uuid, Value::Uuid(rs_uuid)),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("https://demo.example.com").unwrap()
),
// System admins
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset![OAUTH2_SCOPE_OPENID.to_string()]
)
.expect("invalid oauthscope")
)
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(server_txn.create(&ce).is_ok());
// Create a fake session and oauth2 session.
let session_id = Uuid::new_v4();
let parent_id = Uuid::new_v4();
let issued_at = curtime_odt;
let issued_by = IdentityId::User(tuuid);
let scope = SessionScope::ReadOnly;
// Mod the user
let modlist = modlist!([
Modify::Present(
"oauth2_session".into(),
Value::Oauth2Session(
session_id,
Oauth2Session {
parent: Some(parent_id),
// Note we set the exp to None so we are not removing based on exp
state: SessionState::NeverExpires,
issued_at,
rs_uuid,
},
)
),
Modify::Present(
Attribute::UserAuthTokenSession.into(),
Value::Session(
parent_id,
Session {
label: "label".to_string(),
// Note we set the exp to None so we are not removing based on removal of the parent.
state: SessionState::NeverExpires,
// Need the other inner bits?
// for the gracewindow.
issued_at,
// Who actually created this?
issued_by,
cred_id,
// What is the access scope of this session? This is
// for auditing purposes.
scope,
},
)
),
]);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Still there
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&parent_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::NeverExpires));
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::NeverExpires));
// We need the time to be past grace_window.
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(exp_curtime).await;
// Mod again - remove the parent session.
let modlist = ModifyList::new_remove(
Attribute::UserAuthTokenSession.into(),
PartialValue::Refer(parent_id),
);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Session gone.
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
// Note the uat is removed
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&parent_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::RevokedAt(_)));
// The oauth2 session is also removed.
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session.into())
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::RevokedAt(_)));
assert!(server_txn.commit().is_ok());
}
// Test if an oauth2 session exists, the grace window passes and it's UAT doesn't exist.
#[qs_test]
async fn test_session_consistency_oauth2_grace_window_past(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
// Set exp to gracewindow.
let exp_curtime = curtime + GRACE_WINDOW;
// let exp_curtime_odt = OffsetDateTime::UNIX_EPOCH + exp_curtime;
// Create a user
let mut server_txn = server.write(curtime).await;
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
let rs_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(tuuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(
Attribute::Class,
EntryClass::OAuth2ResourceServerBasic.to_value()
),
(Attribute::Uuid, Value::Uuid(rs_uuid)),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOrigin,
Value::new_url_s("https://demo.example.com").unwrap()
),
// System admins
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
UUID_IDM_ALL_ACCOUNTS,
btreeset![OAUTH2_SCOPE_OPENID.to_string()]
)
.expect("invalid oauthscope")
)
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
assert!(server_txn.create(&ce).is_ok());
// Create a fake session.
let session_id = Uuid::new_v4();
let parent = Uuid::new_v4();
let issued_at = curtime_odt;
let session = Value::Oauth2Session(
session_id,
Oauth2Session {
parent: Some(parent),
// Note we set the exp to None so we are asserting the removal is due to the lack
// of the parent session.
state: SessionState::NeverExpires,
issued_at,
rs_uuid,
},
);
// Mod the user
let modlist = ModifyList::new_append(Attribute::OAuth2Session, session);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Still there
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session.into())
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::NeverExpires));
assert!(server_txn.commit().is_ok());
// Note the exp_curtime now is past the gracewindow. This will trigger
// consistency to purge the un-matched session.
let mut server_txn = server.write(exp_curtime).await;
// Mod again - anything will do.
let modlist = ModifyList::new_purge_and_set(
Attribute::Description,
Value::new_utf8s("test person 1 change"),
);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Session gone.
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
// Note it's a not condition now.
let session = entry
.get_ava_as_oauth2session_map(Attribute::OAuth2Session.into())
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::RevokedAt(_)));
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_session_consistency_expire_when_cred_removed(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let curtime_odt = OffsetDateTime::UNIX_EPOCH + curtime;
let p = CryptoPolicy::minimum();
let cred = Credential::new_password_only(&p, "test_password").unwrap();
let cred_id = cred.uuid;
// Create a user
let mut server_txn = server.write(curtime).await;
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(tuuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1")),
(
Attribute::PrimaryCredential,
Value::Cred("primary".to_string(), cred.clone())
)
);
let ce = CreateEvent::new_internal(vec![e1]);
assert!(server_txn.create(&ce).is_ok());
// Create a fake session.
let session_id = Uuid::new_v4();
// No expiry!
let issued_at = curtime_odt;
let issued_by = IdentityId::User(tuuid);
let scope = SessionScope::ReadOnly;
let session = Value::Session(
session_id,
Session {
label: "label".to_string(),
state: SessionState::NeverExpires,
// Need the other inner bits?
// for the gracewindow.
issued_at,
// Who actually created this?
issued_by,
cred_id,
// What is the access scope of this session? This is
// for auditing purposes.
scope,
},
);
// Mod the user
let modlist = ModifyList::new_append(Attribute::UserAuthTokenSession, session);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Still there
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::NeverExpires));
assert!(server_txn.commit().is_ok());
// Notice we keep the time the same for the txn.
let mut server_txn = server.write(curtime).await;
// Remove the primary credential
let modlist = ModifyList::new_purge(Attribute::PrimaryCredential);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
// Session gone.
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
// Note it's a not condition now.
let session = entry
.get_ava_as_session_map(Attribute::UserAuthTokenSession)
.and_then(|sessions| sessions.get(&session_id))
.expect("No session map found");
assert!(matches!(session.state, SessionState::RevokedAt(_)));
assert!(server_txn.commit().is_ok());
}
}