use super::modify::ModifyPartial;
use crate::event::ReviveRecycledEvent;
use crate::prelude::*;
use crate::server::Plugins;
use hashbrown::HashMap;
impl<'a> QueryServerWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)]
pub fn purge_tombstones(&mut self) -> Result<usize, OperationError> {
let trim_cid = self.trim_cid().clone();
let anchor_cid = self.get_txn_cid().clone();
self.be_txn
.reap_tombstones(&anchor_cid, &trim_cid)
.map_err(|e| {
error!(err = ?e, "Tombstone purge operation failed (backend)");
e
})
.inspect(|_res| {
admin_info!("Tombstone purge operation success");
})
}
#[instrument(level = "debug", skip_all)]
pub fn purge_recycled(&mut self) -> Result<usize, OperationError> {
let cid = self.cid.sub_secs(RECYCLEBIN_MAX_AGE).map_err(|e| {
admin_error!(err = ?e, "Unable to generate search cid for purge_recycled");
e
})?;
let rc = self.internal_search(filter_all!(f_and!([
f_eq(Attribute::Class, EntryClass::Recycled.into()),
f_lt(Attribute::LastModifiedCid, PartialValue::new_cid(cid)),
])))?;
if rc.is_empty() {
admin_debug!("No recycled items present - purge operation success");
return Ok(0);
}
let tombstone_cand: Result<Vec<_>, _> = rc
.iter()
.map(|e| {
e.to_tombstone(self.cid.clone())
.validate(&self.schema)
.map_err(|e| {
admin_error!("Schema Violation in purge_recycled validate: {:?}", e);
OperationError::SchemaViolation(e)
})
.map(|e| e.seal(&self.schema))
})
.collect();
let tombstone_cand = tombstone_cand?;
let touched = tombstone_cand.len();
self.be_txn
.modify(&self.cid, &rc, &tombstone_cand)
.map_err(|e| {
admin_error!("Purge recycled operation failed (backend), {:?}", e);
e
})
.map(|_| {
admin_info!("Purge recycled operation success");
touched
})
}
#[instrument(level = "debug", skip_all)]
pub fn revive_recycled(&mut self, re: &ReviveRecycledEvent) -> Result<(), OperationError> {
if !re.ident.is_internal() {
security_info!(name = %re.ident, "revive initiator");
}
let pre_candidates =
self.impersonate_search_valid(re.filter.clone(), re.filter.clone(), &re.ident)?;
if pre_candidates.is_empty() {
if re.ident.is_internal() {
trace!(
"revive: no candidates match filter ... continuing {:?}",
re.filter
);
return Ok(());
} else {
request_error!(
"revive: no candidates match filter, failure {:?}",
re.filter
);
return Err(OperationError::NoMatchingEntries);
}
};
trace!("revive: pre_candidates -> {:?}", pre_candidates);
let modlist = ModifyList::new_list(vec![Modify::Removed(
Attribute::Class,
EntryClass::Recycled.into(),
)]);
let m_valid = modlist.validate(self.get_schema()).map_err(|e| {
admin_error!("revive recycled modlist Schema Violation {:?}", e);
OperationError::SchemaViolation(e)
})?;
let me =
ModifyEvent::new_impersonate(&re.ident, re.filter.clone(), re.filter.clone(), m_valid);
let access = self.get_accesscontrols();
let op_allow = access
.modify_allow_operation(&me, &pre_candidates)
.map_err(|e| {
admin_error!("Unable to check modify access {:?}", e);
e
})?;
if !op_allow {
return Err(OperationError::AccessDenied);
}
if pre_candidates.iter().all(|e| e.mask_recycled().is_some()) {
admin_warn!("Refusing to revive entries that are already live!");
return Err(OperationError::AccessDenied);
}
let mut dm_mods: HashMap<Uuid, ModifyList<ModifyInvalid>> =
HashMap::with_capacity(pre_candidates.len());
for e in &pre_candidates {
let u: Uuid = e.get_uuid();
if let Some(riter) = e.get_ava_as_refuuid(Attribute::RecycledDirectMemberOf) {
for g_uuid in riter {
dm_mods
.entry(g_uuid)
.and_modify(|mlist| {
let m = Modify::Present(Attribute::Member, Value::Refer(u));
mlist.push_mod(m);
})
.or_insert({
let m = Modify::Present(Attribute::Member, Value::Refer(u));
ModifyList::new_list(vec![m])
});
}
}
}
let mut candidates: Vec<Entry<EntryInvalid, EntryCommitted>> = pre_candidates
.iter()
.map(|er| {
er.as_ref()
.clone()
.invalidate(self.cid.clone(), &self.trim_cid)
})
.map(|er| er.to_revived())
.collect();
if candidates.iter().all(|e| e.mask_recycled().is_none()) {
admin_error!("Not all candidates were correctly revived, unable to proceed");
return Err(OperationError::InvalidEntryState);
}
Plugins::run_pre_modify(self, &pre_candidates, &mut candidates, &me).map_err(|e| {
admin_error!("Revive operation failed (plugin), {:?}", e);
e
})?;
let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, OperationError> = candidates
.into_iter()
.map(|e| {
e.validate(&self.schema)
.map_err(|e| {
admin_error!("Schema Violation {:?}", e);
OperationError::SchemaViolation(e)
})
.map(|e| e.seal(&self.schema))
})
.collect();
let norm_cand: Vec<Entry<_, _>> = res?;
let mp = ModifyPartial {
norm_cand,
pre_candidates,
me: &me,
};
self.modify_apply(mp)?;
for (g, mods) in dm_mods {
let f = filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(g)));
self.internal_modify(&f, &mods)?;
}
Ok(())
}
#[cfg(test)]
pub(crate) fn internal_revive_uuid(&mut self, target_uuid: Uuid) -> Result<(), OperationError> {
let filter = filter_rec!(f_eq(Attribute::Uuid, PartialValue::Uuid(target_uuid)));
let f_valid = filter
.validate(self.get_schema())
.map_err(OperationError::SchemaViolation)?;
let re = ReviveRecycledEvent::new_internal(f_valid);
self.revive_recycled(&re)
}
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::event::{CreateEvent, DeleteEvent};
use crate::server::ModifyEvent;
use crate::server::SearchEvent;
use super::ReviveRecycledEvent;
#[qs_test]
async fn test_recycle_simple(server: &QueryServer) {
let time_p1 = duration_from_epoch_now();
let time_p2 = time_p1 + Duration::from_secs(RECYCLEBIN_MAX_AGE * 2);
let mut server_txn = server.write(time_p1).await.unwrap();
let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
let filt_i_rc = filter_all!(f_eq(Attribute::Class, EntryClass::Recycled.into()));
let filt_i_ts = filter_all!(f_eq(Attribute::Class, EntryClass::Tombstone.into()));
let filt_i_per = filter_all!(f_eq(Attribute::Class, EntryClass::Person.into()));
let me_rc = ModifyEvent::new_impersonate_entry(
admin.clone(),
filt_i_rc.clone(),
ModifyList::new_list(vec![Modify::Present(
Attribute::Class,
EntryClass::Recycled.into(),
)]),
);
let de_rc = DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_rc.clone());
let se_rc = SearchEvent::new_ext_impersonate_entry(admin.clone(), filt_i_rc.clone());
let sre_rc = SearchEvent::new_rec_impersonate_entry(admin.clone(), filt_i_rc.clone());
let rre_rc = ReviveRecycledEvent::new_impersonate_entry(
admin,
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
);
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(
Attribute::Uuid,
Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
),
(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::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson2")),
(
Attribute::Uuid,
Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63932"))
),
(Attribute::Description, Value::new_utf8s("testperson2")),
(Attribute::DisplayName, Value::new_utf8s("testperson2"))
);
let ce = CreateEvent::new_internal(vec![e1, e2]);
let cr = server_txn.create(&ce);
assert!(cr.is_ok());
let de_sin = DeleteEvent::new_internal_invalid(filter!(f_or!([
f_eq(Attribute::Name, PartialValue::new_iname("testperson1")),
f_eq(Attribute::Name, PartialValue::new_iname("testperson2")),
])));
assert!(server_txn.delete(&de_sin).is_ok());
let r1 = server_txn.search(&se_rc).expect("search failed");
assert!(r1.is_empty());
assert!(server_txn.delete(&de_rc).is_err());
assert!(server_txn.modify(&me_rc).is_err());
let r2 = server_txn.search(&sre_rc).expect("search failed");
assert_eq!(r2.len(), 2);
let r2 = server_txn
.internal_search(filt_i_rc.clone())
.expect("internal search failed");
assert_eq!(r2.len(), 2);
assert!(server_txn.revive_recycled(&rre_rc).is_ok());
assert!(server_txn.purge_recycled().is_ok());
let r3 = server_txn
.internal_search(filt_i_rc.clone())
.expect("internal search failed");
assert_eq!(r3.len(), 1);
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(time_p2).await.unwrap();
assert!(server_txn.purge_recycled().is_ok());
let r4 = server_txn
.internal_search(filt_i_rc.clone())
.expect("internal search failed");
assert!(r4.is_empty());
let r5 = server_txn
.internal_search(filt_i_ts.clone())
.expect("internal search failed");
assert_eq!(r5.len(), 1);
let r6 = server_txn
.internal_search(filt_i_per.clone())
.expect("internal search failed");
assert_eq!(r6.len(), 1);
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_qs_recycle_advanced(server: &QueryServer) {
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(
Attribute::Uuid,
Value::Uuid(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let ce = CreateEvent::new_internal(vec![e1]);
let cr = server_txn.create(&ce);
assert!(cr.is_ok());
let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)));
assert!(server_txn.delete(&de_sin).is_ok());
let filt_rc = filter_all!(f_eq(Attribute::Class, EntryClass::Recycled.into()));
let sre_rc = SearchEvent::new_rec_impersonate_entry(admin, filt_rc);
let r2 = server_txn.search(&sre_rc).expect("search failed");
assert_eq!(r2.len(), 1);
let cr = server_txn.create(&ce);
assert!(cr.is_err());
assert!(server_txn.commit().is_ok());
}
#[qs_test]
async fn test_uuid_to_star_recycle(server: &QueryServer) {
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
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(uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930"))
),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let tuuid = uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
let ce = CreateEvent::new_internal(vec![e1]);
let cr = server_txn.create(&ce);
assert!(cr.is_ok());
assert_eq!(
server_txn.uuid_to_rdn(tuuid),
Ok("spn=testperson1@example.com".to_string())
);
assert!(
server_txn.uuid_to_spn(tuuid)
== Ok(Some(Value::new_spn_str("testperson1", "example.com")))
);
assert_eq!(server_txn.name_to_uuid("testperson1"), Ok(tuuid));
let de_sin = DeleteEvent::new_internal_invalid(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)));
assert!(server_txn.delete(&de_sin).is_ok());
assert!(
server_txn.uuid_to_rdn(tuuid)
== Ok("uuid=cc8e95b4-c24f-4d68-ba54-8bed76f63930".to_string())
);
assert_eq!(server_txn.uuid_to_spn(tuuid), Ok(None));
assert!(server_txn.name_to_uuid("testperson1").is_err());
let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
let rre_rc = ReviveRecycledEvent::new_impersonate_entry(
admin,
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
);
assert!(server_txn.revive_recycled(&rre_rc).is_ok());
assert_eq!(
server_txn.uuid_to_rdn(tuuid),
Ok("spn=testperson1@example.com".to_string())
);
assert!(
server_txn.uuid_to_spn(tuuid)
== Ok(Some(Value::new_spn_str("testperson1", "example.com")))
);
assert_eq!(server_txn.name_to_uuid("testperson1"), Ok(tuuid));
}
#[qs_test]
async fn test_tombstone(server: &QueryServer) {
let time_p1 = duration_from_epoch_now();
let time_p2 = time_p1 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
let time_p3 = time_p2 + Duration::from_secs(CHANGELOG_MAX_AGE * 2);
trace!("test_tombstone_start");
let mut server_txn = server.write(time_p1).await.unwrap();
let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
let filt_i_ts = filter_all!(f_eq(Attribute::Class, EntryClass::Tombstone.into()));
let me_ts = ModifyEvent::new_impersonate_entry(
admin.clone(),
filt_i_ts.clone(),
ModifyList::new_list(vec![Modify::Present(
Attribute::Class,
EntryClass::Tombstone.into(),
)]),
);
let de_ts = DeleteEvent::new_impersonate_entry(admin.clone(), filt_i_ts.clone());
let se_ts = SearchEvent::new_ext_impersonate_entry(admin, filt_i_ts.clone());
let e_ts = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(
Attribute::Uuid,
Value::Uuid(uuid!("9557f49c-97a5-4277-a9a5-097d17eb8317"))
),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let ce = CreateEvent::new_internal(vec![e_ts]);
let cr = server_txn.create(&ce);
assert!(cr.is_ok());
let de_sin = DeleteEvent::new_internal_invalid(filter!(f_or!([f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)])));
assert!(server_txn.delete(&de_sin).is_ok());
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(time_p2).await.unwrap();
assert!(server_txn.purge_recycled().is_ok());
let r1 = server_txn.search(&se_ts).expect("search failed");
assert!(r1.is_empty());
assert!(server_txn.delete(&de_ts).is_err());
assert!(server_txn.modify(&me_ts).is_err());
let r2 = server_txn
.internal_search(filt_i_ts.clone())
.expect("internal search failed");
assert_eq!(r2.len(), 1);
assert!(server_txn.purge_tombstones().is_ok());
let r3 = server_txn
.internal_search(filt_i_ts.clone())
.expect("internal search failed");
assert_eq!(r3.len(), 1);
assert!(server_txn.commit().is_ok());
let mut server_txn = server.write(time_p3).await.unwrap();
assert!(server_txn.purge_tombstones().is_ok());
let r4 = server_txn
.internal_search(filt_i_ts)
.expect("internal search failed");
assert!(r4.is_empty());
assert!(server_txn.commit().is_ok());
}
fn create_user(name: &str, uuid: &str) -> Entry<EntryInit, EntryNew> {
entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname(name)),
(
Attribute::Uuid,
#[allow(clippy::panic)]
Value::new_uuid_s(uuid).unwrap_or_else(|| { panic!("{}", Attribute::Uuid) })
),
(Attribute::Description, Value::new_utf8s("testperson-entry")),
(Attribute::DisplayName, Value::new_utf8s(name))
)
}
fn create_group(name: &str, uuid: &str, members: &[&str]) -> Entry<EntryInit, EntryNew> {
#[allow(clippy::panic)]
let mut e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname(name)),
(
Attribute::Uuid,
Value::new_uuid_s(uuid).unwrap_or_else(|| { panic!("{}", Attribute::Uuid) })
),
(Attribute::Description, Value::new_utf8s("testgroup-entry"))
);
members
.iter()
.for_each(|m| e1.add_ava(Attribute::Member, Value::new_refer_s(m).unwrap()));
e1
}
fn check_entry_has_mo(qs: &mut QueryServerWriteTransaction, name: &str, mo: &str) -> bool {
let entry = qs
.internal_search(filter!(f_eq(
Attribute::Name,
PartialValue::new_iname(name)
)))
.unwrap()
.pop()
.unwrap();
trace!(?entry);
entry.attribute_equality(Attribute::MemberOf, &PartialValue::new_refer_s(mo).unwrap())
}
#[qs_test]
async fn test_revive_advanced_directmemberships(server: &QueryServer) {
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let admin = server_txn.internal_search_uuid(UUID_ADMIN).expect("failed");
let u1 = create_user("u1", "22b47373-d123-421f-859e-9ddd8ab14a2a");
let g1 = create_group(
"g1",
"cca2bbfc-5b43-43f3-be9e-f5b03b3defec",
&["22b47373-d123-421f-859e-9ddd8ab14a2a"],
);
let u2 = create_user("u2", "5c19a4a2-b9f0-4429-b130-5782de5fddda");
let g2a = create_group(
"g2a",
"e44cf9cd-9941-44cb-a02f-307b6e15ac54",
&["5c19a4a2-b9f0-4429-b130-5782de5fddda"],
);
let g2b = create_group(
"g2b",
"d3132e6e-18ce-4b87-bee1-1d25e4bfe96d",
&["e44cf9cd-9941-44cb-a02f-307b6e15ac54"],
);
let u3 = create_user("u3", "68467a41-6e8e-44d0-9214-a5164e75ca03");
let g3 = create_group(
"g3",
"36048117-e479-45ed-aeb5-611e8d83d5b1",
&["68467a41-6e8e-44d0-9214-a5164e75ca03"],
);
let u4 = create_user("u4", "d696b10f-1729-4f1a-83d0-ca06525c2f59");
let g4 = create_group(
"g4",
"d5c59ac6-c533-4b00-989f-d0e183f07bab",
&["d696b10f-1729-4f1a-83d0-ca06525c2f59"],
);
let ce = CreateEvent::new_internal(vec![u1, g1, u2, g2a, g2b, u3, g3, u4, g4]);
let cr = server_txn.create(&ce);
assert!(cr.is_ok());
let de = DeleteEvent::new_internal_invalid(filter!(f_or(vec![
f_eq(Attribute::Name, PartialValue::new_iname("u1")),
f_eq(Attribute::Name, PartialValue::new_iname("u2")),
f_eq(Attribute::Name, PartialValue::new_iname("u3")),
f_eq(Attribute::Name, PartialValue::new_iname("g3")),
f_eq(Attribute::Name, PartialValue::new_iname("u4")),
f_eq(Attribute::Name, PartialValue::new_iname("g4"))
])));
assert!(server_txn.delete(&de).is_ok());
let rev1 = ReviveRecycledEvent::new_impersonate_entry(
admin.clone(),
filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u1"))),
);
assert!(server_txn.revive_recycled(&rev1).is_ok());
assert!(check_entry_has_mo(
&mut server_txn,
"u1",
"cca2bbfc-5b43-43f3-be9e-f5b03b3defec"
));
let rev2 = ReviveRecycledEvent::new_impersonate_entry(
admin.clone(),
filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u2"))),
);
assert!(server_txn.revive_recycled(&rev2).is_ok());
assert!(check_entry_has_mo(
&mut server_txn,
"u2",
"e44cf9cd-9941-44cb-a02f-307b6e15ac54"
));
assert!(check_entry_has_mo(
&mut server_txn,
"u2",
"d3132e6e-18ce-4b87-bee1-1d25e4bfe96d"
));
let rev3 = ReviveRecycledEvent::new_impersonate_entry(
admin.clone(),
filter_all!(f_or(vec![
f_eq(Attribute::Name, PartialValue::new_iname("u3")),
f_eq(Attribute::Name, PartialValue::new_iname("g3"))
])),
);
assert!(server_txn.revive_recycled(&rev3).is_ok());
assert!(!check_entry_has_mo(
&mut server_txn,
"u3",
"36048117-e479-45ed-aeb5-611e8d83d5b1"
));
let rev4a = ReviveRecycledEvent::new_impersonate_entry(
admin.clone(),
filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("u4"))),
);
assert!(server_txn.revive_recycled(&rev4a).is_ok());
assert!(!check_entry_has_mo(
&mut server_txn,
"u4",
"d5c59ac6-c533-4b00-989f-d0e183f07bab"
));
let rev4b = ReviveRecycledEvent::new_impersonate_entry(
admin,
filter_all!(f_eq(Attribute::Name, PartialValue::new_iname("g4"))),
);
assert!(server_txn.revive_recycled(&rev4b).is_ok());
assert!(!check_entry_has_mo(
&mut server_txn,
"u4",
"d5c59ac6-c533-4b00-989f-d0e183f07bab"
));
assert!(server_txn.commit().is_ok());
}
}