mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Recycle lifecycle mostly done
This commit is contained in:
parent
ea5af4f369
commit
341f7cd0c5
|
@ -207,7 +207,7 @@ impl<STATE> Entry<EntryInvalid, STATE> {
|
|||
// Get the needed schema type
|
||||
let schema_a_r = schema_attributes.get(&attr_name_normal);
|
||||
|
||||
let avas_normal: Vec<String> = match schema_a_r {
|
||||
let mut avas_normal: Vec<String> = match schema_a_r {
|
||||
Some(schema_a) => {
|
||||
avas.iter()
|
||||
.map(|av| {
|
||||
|
@ -219,6 +219,9 @@ impl<STATE> Entry<EntryInvalid, STATE> {
|
|||
None => avas.clone(),
|
||||
};
|
||||
|
||||
// Ensure they are ordered property.
|
||||
avas_normal.sort_unstable();
|
||||
|
||||
// Should never fail!
|
||||
let _ = new_attrs.insert(attr_name_normal, avas_normal);
|
||||
}
|
||||
|
@ -375,6 +378,24 @@ impl Entry<EntryValid, EntryCommitted> {
|
|||
pub fn compare(&self, rhs: &Entry<EntryValid, EntryNew>) -> bool {
|
||||
self.attrs == rhs.attrs
|
||||
}
|
||||
|
||||
pub fn to_tombstone(&self) -> Self {
|
||||
// Duplicate this to a tombstone entry.
|
||||
let uuid_ava = self.get_ava(&String::from("uuid")).expect("Corrupted entry!");
|
||||
let class_ava = vec!["object".to_string(), "tombstone".to_string()];
|
||||
|
||||
let mut attrs_new: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
|
||||
attrs_new.insert("uuid".to_string(), uuid_ava.clone());
|
||||
attrs_new.insert("class".to_string(), class_ava);
|
||||
|
||||
Entry {
|
||||
valid: EntryValid,
|
||||
state: EntryCommitted,
|
||||
id: self.id,
|
||||
attrs: attrs_new,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<STATE> Entry<EntryValid, STATE> {
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::filter::{Filter, FilterInvalid};
|
|||
use super::proto_v1::Entry as ProtoEntry;
|
||||
use super::proto_v1::{
|
||||
AuthRequest, AuthResponse, AuthStatus, CreateRequest, DeleteRequest, ModifyRequest, Response,
|
||||
SearchRequest, SearchResponse
|
||||
SearchRequest, SearchResponse, SearchRecycledRequest, ReviveRecycledRequest
|
||||
};
|
||||
use actix::prelude::*;
|
||||
use entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
||||
|
@ -94,21 +94,21 @@ impl SearchEvent {
|
|||
pub fn new_impersonate(filter: Filter<FilterInvalid>) -> Self {
|
||||
SearchEvent {
|
||||
internal: false,
|
||||
filter: filter,
|
||||
class: (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rec_request(request: SearchRecycledRequest) -> Self {
|
||||
SearchEvent {
|
||||
filter: Filter::And(vec![
|
||||
Filter::AndNot(Box::new(
|
||||
Filter::Or(vec![
|
||||
Filter::Eq(
|
||||
"class".to_string(),
|
||||
"tombstone".to_string(),
|
||||
),
|
||||
Filter::Eq(
|
||||
"class".to_string(),
|
||||
"recycled".to_string(),
|
||||
)
|
||||
])
|
||||
)),
|
||||
filter
|
||||
]),
|
||||
Filter::Eq(
|
||||
"class".to_string(),
|
||||
"recycled".to_string(),
|
||||
),
|
||||
Filter::from(&request.filter)
|
||||
]),
|
||||
internal: false,
|
||||
class: (),
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +200,21 @@ impl Message for DeleteEvent {
|
|||
impl DeleteEvent {
|
||||
pub fn from_request(request: DeleteRequest) -> Self {
|
||||
DeleteEvent {
|
||||
filter: Filter::from(&request.filter),
|
||||
filter: Filter::And(vec![
|
||||
Filter::AndNot(Box::new(
|
||||
Filter::Or(vec![
|
||||
Filter::Eq(
|
||||
"class".to_string(),
|
||||
"tombstone".to_string(),
|
||||
),
|
||||
Filter::Eq(
|
||||
"class".to_string(),
|
||||
"recycled".to_string(),
|
||||
)
|
||||
])
|
||||
)),
|
||||
Filter::from(&request.filter)
|
||||
]),
|
||||
internal: false,
|
||||
}
|
||||
}
|
||||
|
@ -309,3 +323,28 @@ impl PurgeEvent {
|
|||
PurgeEvent {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReviveRecycledEvent {
|
||||
pub filter: Filter<FilterInvalid>,
|
||||
pub internal: bool,
|
||||
}
|
||||
|
||||
impl Message for ReviveRecycledEvent {
|
||||
type Result = ();
|
||||
}
|
||||
|
||||
impl ReviveRecycledEvent {
|
||||
pub fn from_request(request: ReviveRecycledRequest) -> Self {
|
||||
ReviveRecycledEvent {
|
||||
filter: Filter::And(vec![
|
||||
Filter::Eq(
|
||||
"class".to_string(),
|
||||
"recycled".to_string(),
|
||||
),
|
||||
Filter::from(&request.filter)
|
||||
]),
|
||||
internal: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,3 +168,31 @@ pub enum AuthStatus {
|
|||
pub struct AuthResponse {
|
||||
pub status: AuthStatus,
|
||||
}
|
||||
|
||||
|
||||
/* Recycle Requests area */
|
||||
|
||||
// Only two actions on recycled is possible. Search and Revive.
|
||||
|
||||
pub struct SearchRecycledRequest {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
impl SearchRecycledRequest {
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
SearchRecycledRequest { filter: filter }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Need a search response here later.
|
||||
|
||||
pub struct ReviveRecycledRequest {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
impl ReviveRecycledRequest {
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
ReviveRecycledRequest { filter: filter }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,11 @@ use entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
|||
use error::{OperationError, SchemaError};
|
||||
use event::{
|
||||
AuthEvent, AuthResult, CreateEvent, DeleteEvent, ExistsEvent, ModifyEvent, OpResult,
|
||||
SearchEvent, SearchResult, PurgeEvent,
|
||||
SearchEvent, SearchResult, PurgeEvent, ReviveRecycledEvent
|
||||
};
|
||||
use filter::{Filter, FilterInvalid};
|
||||
use log::EventLog;
|
||||
use modify::ModifyList;
|
||||
use modify::{ModifyList, Modify};
|
||||
use plugins::Plugins;
|
||||
use schema::{Schema, SchemaReadTransaction, SchemaTransaction, SchemaWriteTransaction};
|
||||
|
||||
|
@ -103,6 +103,7 @@ pub trait QueryServerReadTransaction {
|
|||
// performing un-indexed searches on attr's that don't exist in the
|
||||
// server. This is why ExtensibleObject can only take schema that
|
||||
// exists in the server, not arbitrary attr names.
|
||||
audit_log!(au, "search: filter -> {:?}", se.filter);
|
||||
|
||||
// TODO: Normalise the filter
|
||||
|
||||
|
@ -471,14 +472,75 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
res
|
||||
}
|
||||
|
||||
pub fn purge_recycle(&self) -> Result<(), OperationError> {
|
||||
pub fn purge_recycled(&self, au: &mut AuditScope) -> Result<(), OperationError> {
|
||||
// Send everything that is recycled to tombstone
|
||||
unimplemented!()
|
||||
// Search all recycled
|
||||
let rc = match self.internal_search(
|
||||
au,
|
||||
Filter::Eq("class".to_string(), "recycled".to_string())
|
||||
) {
|
||||
Ok(r) => {
|
||||
r
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e)
|
||||
}
|
||||
};
|
||||
|
||||
// Modify them to strip all avas except uuid
|
||||
|
||||
let tombstone_cand = rc.iter().map(|e| {
|
||||
e.to_tombstone()
|
||||
}).collect();
|
||||
|
||||
// Backend Modify
|
||||
let mut audit_be = AuditScope::new("backend_modify");
|
||||
|
||||
let res = self
|
||||
.be_txn
|
||||
.modify(&mut audit_be, &tombstone_cand)
|
||||
.map(|_| ())
|
||||
.map_err(|e| match e {
|
||||
BackendError::EmptyRequest => OperationError::EmptyRequest,
|
||||
BackendError::EntryMissingId => OperationError::InvalidRequestState,
|
||||
});
|
||||
au.append_scope(audit_be);
|
||||
|
||||
if res.is_err() {
|
||||
// be_txn is dropped, ie aborted here.
|
||||
audit_log!(au, "Purge recycled operation failed (backend), {:?}", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
// return
|
||||
audit_log!(au, "Purge recycled operation success");
|
||||
res
|
||||
}
|
||||
|
||||
pub fn revive_recycled(&self) -> Result<(), OperationError> {
|
||||
// Revive an entry to live.
|
||||
unimplemented!()
|
||||
// Should this take a revive event?
|
||||
pub fn revive_recycled(&self, au: &mut AuditScope, re: &ReviveRecycledEvent) -> Result<(), OperationError> {
|
||||
// Revive an entry to live. This is a specialised (limited)
|
||||
// modify proxy.
|
||||
//
|
||||
// impersonate modify will require ability to search the class=recycled
|
||||
// and the ability to remove that from the object.
|
||||
|
||||
// create the modify
|
||||
// tl;dr, remove the class=recycled
|
||||
let modlist = ModifyList::new_list(vec![
|
||||
Modify::Removed(
|
||||
"class".to_string(),
|
||||
"recycled".to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
// Now impersonate the modify
|
||||
self.impersonate_modify(
|
||||
au,
|
||||
re.filter.clone(),
|
||||
modlist
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
pub fn modify(&self, au: &mut AuditScope, me: &ModifyEvent) -> Result<(), OperationError> {
|
||||
|
@ -629,6 +691,19 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
res
|
||||
}
|
||||
|
||||
pub fn impersonate_modify(
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
filter: Filter<FilterInvalid>,
|
||||
modlist: ModifyList,
|
||||
) -> Result<(), OperationError> {
|
||||
let mut audit_int = AuditScope::new("impersonate_modify");
|
||||
let me = ModifyEvent::new_internal(filter, modlist);
|
||||
let res = self.modify(&mut audit_int, &me);
|
||||
audit.append_scope(audit_int);
|
||||
res
|
||||
}
|
||||
|
||||
// internal server operation types.
|
||||
// These just wrap the fn create/search etc, but they allow
|
||||
// creating the needed create event with the correct internal flags
|
||||
|
@ -968,13 +1043,13 @@ mod tests {
|
|||
use super::super::be::{Backend, BackendTransaction};
|
||||
use super::super::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
||||
use super::super::error::OperationError;
|
||||
use super::super::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent};
|
||||
use super::super::event::{CreateEvent, DeleteEvent, ModifyEvent, SearchEvent, ReviveRecycledEvent};
|
||||
use super::super::filter::Filter;
|
||||
use super::super::log;
|
||||
use super::super::modify::{Modify, ModifyList};
|
||||
use super::super::proto_v1::Entry as ProtoEntry;
|
||||
use super::super::proto_v1::Filter as ProtoFilter;
|
||||
use super::super::proto_v1::{CreateRequest, SearchRequest, DeleteRequest, ModifyRequest};
|
||||
use super::super::proto_v1::{CreateRequest, SearchRequest, DeleteRequest, ModifyRequest, SearchRecycledRequest, ReviveRecycledRequest};
|
||||
use super::super::proto_v1::Modify as ProtoModify;
|
||||
use super::super::proto_v1::ModifyList as ProtoModifyList;
|
||||
use super::super::schema::Schema;
|
||||
|
@ -1425,4 +1500,151 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qs_recycle_simple() {
|
||||
run_test!(|_log, mut server: QueryServer, audit: &mut AuditScope| {
|
||||
let mut server_txn = server.write();
|
||||
|
||||
let filt_rc = ProtoFilter::Eq(
|
||||
String::from("class"),
|
||||
String::from("recycled")
|
||||
);
|
||||
|
||||
let filt_i_rc = Filter::Eq(
|
||||
String::from("class"),
|
||||
String::from("recycled")
|
||||
);
|
||||
|
||||
let filt_i_ts = Filter::Eq(
|
||||
String::from("class"),
|
||||
String::from("tombstone")
|
||||
);
|
||||
|
||||
let filt_i_per = Filter::Eq(
|
||||
String::from("class"),
|
||||
String::from("person")
|
||||
);
|
||||
|
||||
// Create fake external requests. Probably from admin later
|
||||
let me_rc = ModifyEvent::from_request(
|
||||
ModifyRequest::new(
|
||||
filt_rc.clone(),
|
||||
ProtoModifyList::new_list(vec![
|
||||
ProtoModify::Present(String::from("class"), String::from("recycled")),
|
||||
]),
|
||||
)
|
||||
);
|
||||
let de_rc = DeleteEvent::from_request(DeleteRequest::new(filt_rc.clone()));
|
||||
let se_rc = SearchEvent::from_request(SearchRequest::new(filt_rc.clone()));
|
||||
|
||||
let sre_rc = SearchEvent::from_rec_request(
|
||||
SearchRecycledRequest::new(
|
||||
filt_rc.clone()
|
||||
)
|
||||
);
|
||||
|
||||
let rre_rc = ReviveRecycledEvent::from_request(
|
||||
ReviveRecycledRequest::new(
|
||||
ProtoFilter::Eq(
|
||||
"name".to_string(),
|
||||
"testperson1".to_string(),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Create some recycled objects
|
||||
let e1: Entry<EntryInvalid, EntryNew> = serde_json::from_str(
|
||||
r#"{
|
||||
"valid": null,
|
||||
"state": null,
|
||||
"attrs": {
|
||||
"class": ["object", "person", "recycled"],
|
||||
"name": ["testperson1"],
|
||||
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63930"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson1"]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let e2: Entry<EntryInvalid, EntryNew> = serde_json::from_str(
|
||||
r#"{
|
||||
"valid": null,
|
||||
"state": null,
|
||||
"attrs": {
|
||||
"class": ["object", "person", "recycled"],
|
||||
"name": ["testperson2"],
|
||||
"uuid": ["cc8e95b4-c24f-4d68-ba54-8bed76f63932"],
|
||||
"description": ["testperson"],
|
||||
"displayname": ["testperson2"]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
let ce = CreateEvent::from_vec(vec![e1, e2]);
|
||||
let cr = server_txn.create(audit, &ce);
|
||||
assert!(cr.is_ok());
|
||||
|
||||
// Can it be seen (external search)
|
||||
let r1 = server_txn.search(audit, &se_rc).unwrap();
|
||||
assert!(r1.len() == 0);
|
||||
|
||||
// Can it be deleted (external delete)
|
||||
// Should be err-no candidates.
|
||||
assert!(server_txn.delete(audit, &de_rc).is_err());
|
||||
|
||||
// Can it be modified? (external modify)
|
||||
// Should be err-no candidates
|
||||
assert!(server_txn.modify(audit, &me_rc).is_err());
|
||||
|
||||
// Can in be seen by special search? (external recycle search)
|
||||
let r2 = server_txn.search(audit, &sre_rc).unwrap();
|
||||
assert!(r2.len() == 2);
|
||||
|
||||
// Can it be seen (internal search)
|
||||
// Internal search should see it.
|
||||
let r2 = server_txn.internal_search(audit, filt_i_rc.clone()).unwrap();
|
||||
assert!(r2.len() == 2);
|
||||
|
||||
// There are now two options
|
||||
// revival
|
||||
assert!(server_txn.revive_recycled(audit, &rre_rc).is_ok());
|
||||
|
||||
// purge to tombstone
|
||||
assert!(server_txn.purge_recycled(audit).is_ok());
|
||||
|
||||
// Should be no recycled objects.
|
||||
let r3 = server_txn.internal_search(audit, filt_i_rc.clone()).unwrap();
|
||||
assert!(r3.len() == 0);
|
||||
|
||||
// There should be one tombstone
|
||||
let r4 = server_txn.internal_search(audit, filt_i_ts.clone()).unwrap();
|
||||
assert!(r4.len() == 1);
|
||||
|
||||
// There should be one entry
|
||||
let r5 = server_txn.internal_search(audit, filt_i_per.clone()).unwrap();
|
||||
assert!(r5.len() == 1);
|
||||
|
||||
assert!(server_txn.commit(audit).is_ok());
|
||||
future::ok(())
|
||||
})
|
||||
}
|
||||
|
||||
// The delete test above should be unaffected by recycle anyway
|
||||
#[test]
|
||||
fn test_qs_recycle_advanced() {
|
||||
run_test!(|_log, mut server: QueryServer, audit: &mut AuditScope| {
|
||||
let mut server_txn = server.write();
|
||||
|
||||
// Create items
|
||||
// Delete and ensure they became recycled.
|
||||
// After a delete -> recycle, create duplicate name etc.
|
||||
// Create dup uuid (rej)
|
||||
assert!(server_txn.commit(audit).is_ok());
|
||||
future::ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue