use std::collections::BTreeSet;
use std::sync::Arc;
use hashbrown::{HashMap, HashSet};
use crate::event::{CreateEvent, DeleteEvent, ModifyEvent};
use crate::filter::{f_eq, FC};
use crate::plugins::Plugin;
use crate::prelude::*;
use crate::schema::{SchemaAttribute, SchemaTransaction};
pub struct ReferentialIntegrity;
impl ReferentialIntegrity {
#[instrument(level = "debug", name = "check_uuids_exist_fast", skip_all)]
fn check_uuids_exist_fast(
qs: &mut QueryServerWriteTransaction,
inner: &[Uuid],
) -> Result<bool, OperationError> {
if inner.is_empty() {
trace!("no reference types modified, skipping check");
return Ok(true);
}
let inner: Vec<_> = inner
.iter()
.map(|u| f_eq(Attribute::Uuid, PartialValue::Uuid(*u)))
.collect();
let filt_in = filter!(f_inc(inner));
let b = qs.internal_exists(filt_in).map_err(|e| {
admin_error!(err = ?e, "internal exists failure");
e
})?;
if b {
Ok(true)
} else {
Ok(false)
}
}
#[instrument(level = "debug", name = "check_uuids_exist_slow", skip_all)]
fn check_uuids_exist_slow(
qs: &mut QueryServerWriteTransaction,
inner: &[Uuid],
) -> Result<Vec<Uuid>, OperationError> {
if inner.is_empty() {
trace!("no reference types modified, skipping check");
return Ok(Vec::with_capacity(0));
}
let mut missing = Vec::with_capacity(inner.len());
for u in inner {
let filt_in = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(*u)));
let b = qs.internal_exists(filt_in).map_err(|e| {
admin_error!(err = ?e, "internal exists failure");
e
})?;
if !b {
missing.push(*u)
}
}
Ok(missing)
}
fn remove_references(
qs: &mut QueryServerWriteTransaction,
uuids: Vec<Uuid>,
) -> Result<(), OperationError> {
trace!(?uuids);
let schema = qs.get_schema();
let ref_types = schema.get_reference_types();
let removed_ids: BTreeSet<_> = uuids.iter().map(|u| PartialValue::Refer(*u)).collect();
let filt = filter_all!(FC::Or(
uuids
.into_iter()
.flat_map(|u| ref_types
.values()
.map(move |r_type| { f_eq(r_type.name.clone(), PartialValue::Refer(u)) }))
.collect(),
));
trace!("refint post_delete filter {:?}", filt);
let mut work_set = qs.internal_search_writeable(&filt)?;
for (_, post) in work_set.iter_mut() {
for schema_attribute in ref_types.values() {
post.remove_avas(&schema_attribute.name, &removed_ids);
}
}
qs.internal_apply_writable(work_set)
}
}
impl Plugin for ReferentialIntegrity {
fn id() -> &'static str {
"referential_integrity"
}
#[instrument(level = "debug", name = "refint_post_create", skip(qs, cand, _ce))]
fn post_create(
qs: &mut QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
_ce: &CreateEvent,
) -> Result<(), OperationError> {
Self::post_modify_inner(qs, None, cand)
}
#[instrument(level = "debug", name = "refint_post_modify", skip_all)]
fn post_modify(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &[Entry<EntrySealed, EntryCommitted>],
_me: &ModifyEvent,
) -> Result<(), OperationError> {
Self::post_modify_inner(qs, Some(pre_cand), cand)
}
#[instrument(level = "debug", name = "refint_post_batch_modify", skip_all)]
fn post_batch_modify(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &[Entry<EntrySealed, EntryCommitted>],
_me: &BatchModifyEvent,
) -> Result<(), OperationError> {
Self::post_modify_inner(qs, Some(pre_cand), cand)
}
#[instrument(level = "debug", name = "refint_post_repl_refresh", skip_all)]
fn post_repl_refresh(
qs: &mut QueryServerWriteTransaction,
cand: &[EntrySealedCommitted],
) -> Result<(), OperationError> {
Self::post_modify_inner(qs, None, cand)
}
#[instrument(level = "debug", name = "refint_post_repl_incremental", skip_all)]
fn post_repl_incremental(
qs: &mut QueryServerWriteTransaction,
pre_cand: &[Arc<EntrySealedCommitted>],
cand: &[EntrySealedCommitted],
conflict_uuids: &BTreeSet<Uuid>,
) -> Result<(), OperationError> {
let uuids = Self::cand_references_to_uuid_filter(qs, Some(pre_cand), cand)?;
let all_exist_fast = Self::check_uuids_exist_fast(qs, uuids.as_slice())?;
let mut missing_uuids = if !all_exist_fast {
debug!("Not all uuids referenced by these candidates exist. Slow path to remove them.");
Self::check_uuids_exist_slow(qs, uuids.as_slice())?
} else {
debug!("All references are valid!");
Vec::with_capacity(0)
};
let inactive_entries: Vec<_> = std::iter::zip(pre_cand, cand)
.filter_map(|(pre, post)| {
let pre_live = pre.mask_recycled_ts().is_some();
let post_live = post.mask_recycled_ts().is_some();
if !post_live && (pre_live != post_live) {
Some(post.get_uuid())
} else {
None
}
})
.collect();
if event_enabled!(tracing::Level::DEBUG) {
debug!("Removing the following reference uuids for entries that have become recycled or tombstoned");
for missing in &inactive_entries {
debug!(?missing);
}
}
if !conflict_uuids.is_empty() {
warn!("conflict uuids have been found, and must be cleaned from existing references. This is to prevent group memberships leaking to un-intended recipients.");
}
missing_uuids.extend(conflict_uuids.iter().copied());
missing_uuids.extend_from_slice(&inactive_entries);
if missing_uuids.is_empty() {
trace!("Nothing to do, shortcut");
return Ok(());
}
if event_enabled!(tracing::Level::DEBUG) {
debug!("Removing the following missing reference uuids");
for missing in &missing_uuids {
debug!(?missing);
}
}
Self::remove_references(qs, missing_uuids)
}
#[instrument(level = "debug", name = "refint_post_delete", skip_all)]
fn post_delete(
qs: &mut QueryServerWriteTransaction,
cand: &[Entry<EntrySealed, EntryCommitted>],
_ce: &DeleteEvent,
) -> Result<(), OperationError> {
let uuids: Vec<Uuid> = cand.iter().map(|e| e.get_uuid()).collect();
Self::remove_references(qs, uuids)
}
#[instrument(level = "debug", name = "refint::verify", skip_all)]
fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
let filt_in = filter_all!(f_pres(Attribute::Class));
let all_cand = match qs
.internal_search(filt_in)
.map_err(|_| Err(ConsistencyError::QueryServerSearchFailure))
{
Ok(all_cand) => all_cand,
Err(e) => return vec![e],
};
let acu_map: HashSet<Uuid> = all_cand.iter().map(|e| e.get_uuid()).collect();
let schema = qs.get_schema();
let ref_types = schema.get_reference_types();
let mut res = Vec::with_capacity(0);
for c in &all_cand {
for rtype in ref_types.values() {
if let Some(vs) = c.get_ava_set(&rtype.name) {
match vs.as_ref_uuid_iter() {
Some(uuid_iter) => {
for vu in uuid_iter {
if acu_map.get(&vu).is_none() {
res.push(Err(ConsistencyError::RefintNotUpheld(c.get_id())))
}
}
}
None => res.push(Err(ConsistencyError::InvalidAttributeType(
"A non-value-ref type was found.".to_string(),
))),
}
}
}
}
res
}
}
fn update_reference_set<'a, I>(
ref_types: &HashMap<Attribute, SchemaAttribute>,
entry_iter: I,
reference_set: &mut BTreeSet<Uuid>,
) -> Result<(), OperationError>
where
I: Iterator<Item = &'a EntrySealedCommitted>,
{
for cand in entry_iter {
trace!(cand_id = %cand.get_display_id());
let dyn_group = cand.attribute_equality(Attribute::Class, &EntryClass::DynGroup.into());
let cand_ref_valuesets = ref_types.values().filter_map(|rtype| {
let skip_mb = dyn_group && rtype.name == Attribute::DynMember;
let skip_mo = rtype.name == Attribute::MemberOf;
if skip_mb || skip_mo {
None
} else {
cand.get_ava_set(&rtype.name)
}
});
for vs in cand_ref_valuesets {
if let Some(uuid_iter) = vs.as_ref_uuid_iter() {
reference_set.extend(uuid_iter);
Ok(())
} else {
error!(?vs, "reference value could not convert to reference uuid.");
error!("If you are sure the name/uuid/spn exist, and that this is in error, you should run a verify task.");
Err(OperationError::InvalidAttribute(
"uuid could not become reference value".to_string(),
))
}?;
}
}
Ok(())
}
impl ReferentialIntegrity {
fn cand_references_to_uuid_filter(
qs: &mut QueryServerWriteTransaction,
pre_cand: Option<&[Arc<EntrySealedCommitted>]>,
post_cand: &[EntrySealedCommitted],
) -> Result<Vec<Uuid>, OperationError> {
let schema = qs.get_schema();
let ref_types = schema.get_reference_types();
let mut previous_reference_set = BTreeSet::new();
let mut reference_set = BTreeSet::new();
if let Some(pre_cand) = pre_cand {
update_reference_set(
ref_types,
pre_cand.iter().map(|e| e.as_ref()),
&mut previous_reference_set,
)?;
}
update_reference_set(ref_types, post_cand.iter(), &mut reference_set)?;
Ok(reference_set
.difference(&previous_reference_set)
.copied()
.collect())
}
fn post_modify_inner(
qs: &mut QueryServerWriteTransaction,
pre_cand: Option<&[Arc<EntrySealedCommitted>]>,
post_cand: &[EntrySealedCommitted],
) -> Result<(), OperationError> {
let uuids = Self::cand_references_to_uuid_filter(qs, pre_cand, post_cand)?;
let all_exist_fast = Self::check_uuids_exist_fast(qs, uuids.as_slice())?;
if all_exist_fast {
return Ok(());
}
let missing_uuids = Self::check_uuids_exist_slow(qs, uuids.as_slice())?;
error!("some uuids that were referenced in this operation do not exist.");
for missing in missing_uuids {
error!(?missing);
}
Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
"Uuid referenced not found in database".to_string(),
)))
}
}
#[cfg(test)]
mod tests {
use kanidm_proto::internal::Filter as ProtoFilter;
use crate::event::CreateEvent;
use crate::prelude::*;
use crate::value::{AuthType, Oauth2Session, OauthClaimMapJoin, Session, SessionState};
use time::OffsetDateTime;
use crate::credential::Credential;
use kanidm_lib_crypto::CryptoPolicy;
const TEST_TESTGROUP_A_UUID: &str = "d2b496bd-8493-47b7-8142-f568b5cf47ee";
const TEST_TESTGROUP_B_UUID: &str = "8cef42bc-2cac-43e4-96b3-8f54561885ca";
#[test]
fn test_create_uuid_reference_not_exist() {
let e = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
)
);
let create = vec![e];
let preload = Vec::with_capacity(0);
run_create_test!(
Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
"Uuid referenced not found in database".to_string()
))),
preload,
create,
None,
|_| {}
);
}
#[test]
fn test_create_uuid_reference_exist() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Uuid,
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let eb = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let preload = vec![ea];
let create = vec![eb];
run_create_test!(
Ok(()),
preload,
create,
None,
|qs: &mut QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)))
.expect("Internal search failure");
let _ue = cands.first().expect("No cand");
}
);
}
#[test]
fn test_create_uuid_reference_self() {
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::with_capacity(0);
let id = uuid::uuid!("8cef42bc-2cac-43e4-96b3-8f54561885ca");
let e_group = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Uuid, Value::Uuid(id)),
(Attribute::Member, Value::Refer(id))
);
let create = vec![e_group];
run_create_test!(
Ok(()),
preload,
create,
None,
|qs: &mut QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup")
)))
.expect("Internal search failure");
let _ue = cands.first().expect("No cand");
}
);
}
#[test]
fn test_modify_uuid_reference_exist() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
)
);
let eb = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b"))
);
let preload = vec![ea, eb];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::Member,
Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_uuid_reference_not_exist() {
let eb = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b"))
);
let preload = vec![eb];
run_modify_test!(
Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
"Uuid referenced not found in database".to_string()
))),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::Member,
Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_uuid_reference_partial_not_exist() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
)
);
let eb = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b"))
);
let preload = vec![ea, eb];
run_modify_test!(
Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
"Uuid referenced not found in database".to_string()
))),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
ModifyList::new_list(vec![
Modify::Present(
Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
),
Modify::Present(Attribute::Member, Value::Refer(UUID_DOES_NOT_EXIST)),
]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_remove_referee() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
)
);
let eb = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b")),
(
Attribute::Member,
Value::Refer(uuid::uuid!(TEST_TESTGROUP_A_UUID))
)
);
let preload = vec![ea, eb];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
ModifyList::new_list(vec![Modify::Purged(Attribute::Member)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_uuid_reference_self() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
)
);
let preload = vec![ea];
run_modify_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::Member,
Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]),
None,
|_| {},
|_| {}
);
}
#[test]
fn test_modify_reference_deleted() {
let ea = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(
Attribute::Uuid,
Value::Uuid(uuid::uuid!(TEST_TESTGROUP_A_UUID))
)
);
let eb = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b"))
);
let preload = vec![ea, eb];
run_modify_test!(
Err(OperationError::Plugin(PluginError::ReferentialIntegrity(
"Uuid referenced not found in database".to_string()
))),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
ModifyList::new_list(vec![Modify::Present(
Attribute::Member,
Value::new_refer_s(TEST_TESTGROUP_A_UUID).unwrap()
)]),
None,
|qs: &mut QueryServerWriteTransaction| {
let de_sin =
crate::event::DeleteEvent::new_internal_invalid(filter!(f_or!([f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)])));
assert!(qs.delete(&de_sin).is_ok());
},
|_| {}
);
}
#[test]
fn test_delete_remove_referent_valid() {
let ea: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Uuid,
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let eb: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let preload = vec![ea, eb];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)),
None,
|_qs: &mut QueryServerWriteTransaction| {}
);
}
#[test]
fn test_delete_remove_referent_invalid() {
let target_uuid = Uuid::new_v4();
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(Attribute::Uuid, Value::Uuid(target_uuid))
);
let e_acp: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(
Attribute::Class,
EntryClass::AccessControlProfile.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlReceiverGroup.to_value()
),
(
Attribute::Class,
EntryClass::AccessControlTargetScope.to_value()
),
(Attribute::Name, Value::new_iname("acp_referer")),
(Attribute::AcpReceiverGroup, Value::Refer(target_uuid)),
(
Attribute::AcpTargetScope,
Value::new_json_filter_s("{\"eq\":[\"name\",\"a\"]}").expect("filter")
)
);
let preload = vec![e_group, e_acp];
run_delete_test!(
Err(OperationError::SchemaViolation(
SchemaError::MissingMustAttribute(vec![Attribute::AcpReceiverGroup])
)),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_a")
)),
None,
|_qs: &mut QueryServerWriteTransaction| {}
);
}
#[test]
fn test_delete_remove_referee() {
let ea: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_a")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Uuid,
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let eb: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let preload = vec![ea, eb];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
None,
|_qs: &mut QueryServerWriteTransaction| {}
);
}
#[test]
fn test_delete_remove_reference_self() {
let eb: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup_b")),
(Attribute::Description, Value::new_utf8s("testgroup")),
(
Attribute::Uuid,
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
),
(
Attribute::Member,
Value::Refer(Uuid::parse_str(TEST_TESTGROUP_A_UUID).unwrap())
)
);
let preload = vec![eb];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testgroup_b")
)),
None,
|_qs: &mut QueryServerWriteTransaction| {}
);
}
#[test]
fn test_delete_remove_reference_oauth2() {
let ea: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOriginLanding,
Value::new_url_s("https://demo.example.com").unwrap()
),
(
Attribute::OAuth2RsScopeMap,
Value::new_oauthscopemap(
Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap(),
btreeset![OAUTH2_SCOPE_READ.to_string()]
)
.expect("Invalid scope")
)
);
let eb: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(
Attribute::Uuid,
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
),
(Attribute::Description, Value::new_utf8s("testgroup"))
);
let preload = vec![ea, eb];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
None,
|qs: &mut QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_resource_server")
)))
.expect("Internal search failure");
let ue = cands.first().expect("No entry");
assert!(ue
.get_ava_as_oauthscopemaps(Attribute::OAuth2RsScopeMap)
.is_none())
}
);
}
#[qs_test]
async fn test_delete_oauth2_rs_remove_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 mut server_txn = server.write(curtime).await.unwrap();
let tuuid = Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap();
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::Uuid, Value::Uuid(rs_uuid)),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOriginLanding,
Value::new_url_s("https://demo.example.com").unwrap()
),
(
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());
let session_id = Uuid::new_v4();
let pv_session_id = PartialValue::Refer(session_id);
let parent_id = Uuid::new_v4();
let pv_parent_id = PartialValue::Refer(parent_id);
let issued_at = curtime_odt;
let issued_by = IdentityId::User(tuuid);
let scope = SessionScope::ReadOnly;
let modlist = modlist!([
Modify::Present(
Attribute::OAuth2Session,
Value::Oauth2Session(
session_id,
Oauth2Session {
parent: Some(parent_id),
state: SessionState::NeverExpires,
issued_at,
rs_uuid,
},
)
),
Modify::Present(
Attribute::UserAuthTokenSession,
Value::Session(
parent_id,
Session {
label: "label".to_string(),
state: SessionState::NeverExpires,
issued_at,
issued_by,
cred_id,
scope,
type_: AuthType::Passkey,
},
)
),
]);
server_txn
.internal_modify(
&filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(tuuid))),
&modlist,
)
.expect("Failed to modify user");
let entry = server_txn.internal_search_uuid(tuuid).expect("failed");
assert!(entry.attribute_equality(Attribute::UserAuthTokenSession, &pv_parent_id));
assert!(entry.attribute_equality(Attribute::OAuth2Session, &pv_session_id));
assert!(server_txn.internal_delete_uuid(rs_uuid).is_ok());
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::RevokedAt(_)));
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_ignore_references_for_regen(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let mut server_txn = server.write(curtime).await.unwrap();
let tgroup_uuid = Uuid::new_v4();
let dyn_uuid = Uuid::new_v4();
let inv_mo_uuid = Uuid::new_v4();
let inv_mb_uuid = Uuid::new_v4();
let e_dyn = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Class, EntryClass::DynGroup.to_value()),
(Attribute::Uuid, Value::Uuid(dyn_uuid)),
(Attribute::Name, Value::new_iname("test_dyngroup")),
(Attribute::DynMember, Value::Refer(inv_mb_uuid)),
(
Attribute::DynGroupFilter,
Value::JsonFilt(ProtoFilter::Eq(
Attribute::Name.to_string(),
"testgroup".to_string()
))
)
);
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Class, EntryClass::MemberOf.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Uuid, Value::Uuid(tgroup_uuid)),
(Attribute::MemberOf, Value::Refer(inv_mo_uuid))
);
let ce = CreateEvent::new_internal(vec![e_dyn, e_group]);
assert!(server_txn.create(&ce).is_ok());
let dyna = server_txn
.internal_search_uuid(dyn_uuid)
.expect("Failed to access dyn group");
let dyn_member = dyna
.get_ava_refer(Attribute::DynMember)
.expect("Failed to get dyn member attribute");
assert_eq!(dyn_member.len(), 1);
assert!(dyn_member.contains(&tgroup_uuid));
let group = server_txn
.internal_search_uuid(tgroup_uuid)
.expect("Failed to access mo group");
let grp_member = group
.get_ava_refer(Attribute::MemberOf)
.expect("Failed to get memberof attribute");
assert_eq!(grp_member.len(), 1);
assert!(grp_member.contains(&dyn_uuid));
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_entry_managed_by_references(server: &QueryServer) {
let curtime = duration_from_epoch_now();
let mut server_txn = server.write(curtime).await.unwrap();
let manages_uuid = Uuid::new_v4();
let e_manages: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("entry_manages")),
(Attribute::Uuid, Value::Uuid(manages_uuid))
);
let group_uuid = Uuid::new_v4();
let e_group: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("entry_managed_by")),
(Attribute::Uuid, Value::Uuid(group_uuid)),
(Attribute::EntryManagedBy, Value::Refer(manages_uuid))
);
let ce = CreateEvent::new_internal(vec![e_manages, e_group]);
assert!(server_txn.create(&ce).is_ok());
let group = server_txn
.internal_search_uuid(group_uuid)
.expect("Failed to access group");
let entry_managed_by = group
.get_ava_single_refer(Attribute::EntryManagedBy)
.expect("No entry managed by");
assert_eq!(entry_managed_by, manages_uuid);
assert!(server_txn.internal_delete_uuid(manages_uuid).is_ok());
let group = server_txn
.internal_search_uuid(group_uuid)
.expect("Failed to access group");
assert!(group.get_ava_refer(Attribute::EntryManagedBy).is_none());
assert!(server_txn.commit().is_ok());
}
#[test]
fn test_delete_remove_reference_oauth2_claim_map() {
let ea: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(
Attribute::Class,
EntryClass::OAuth2ResourceServer.to_value()
),
(
Attribute::Class,
EntryClass::OAuth2ResourceServerPublic.to_value()
),
(Attribute::Name, Value::new_iname("test_resource_server")),
(
Attribute::DisplayName,
Value::new_utf8s("test_resource_server")
),
(
Attribute::OAuth2RsOriginLanding,
Value::new_url_s("https://demo.example.com").unwrap()
),
(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimMap(
"custom_a".to_string(),
OauthClaimMapJoin::CommaSeparatedValue,
)
),
(
Attribute::OAuth2RsClaimMap,
Value::OauthClaimValue(
"custom_a".to_string(),
Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap(),
btreeset!["value_a".to_string()],
)
)
);
let eb: Entry<EntryInit, EntryNew> = entry_init!(
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(
Attribute::Uuid,
Value::Uuid(Uuid::parse_str(TEST_TESTGROUP_B_UUID).unwrap())
),
(Attribute::Description, Value::new_utf8s("testgroup"))
);
let preload = vec![ea, eb];
run_delete_test!(
Ok(()),
preload,
filter!(f_eq(Attribute::Name, PartialValue::new_iname("testgroup"))),
None,
|qs: &mut QueryServerWriteTransaction| {
let cands = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_resource_server")
)))
.expect("Internal search failure");
let ue = cands.first().expect("No entry");
assert!(ue
.get_ava_set(Attribute::OAuth2RsClaimMap)
.and_then(|vs| vs.as_oauthclaim_map())
.is_none())
}
);
}
}