mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +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
|
// Get the needed schema type
|
||||||
let schema_a_r = schema_attributes.get(&attr_name_normal);
|
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) => {
|
Some(schema_a) => {
|
||||||
avas.iter()
|
avas.iter()
|
||||||
.map(|av| {
|
.map(|av| {
|
||||||
|
@ -219,6 +219,9 @@ impl<STATE> Entry<EntryInvalid, STATE> {
|
||||||
None => avas.clone(),
|
None => avas.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ensure they are ordered property.
|
||||||
|
avas_normal.sort_unstable();
|
||||||
|
|
||||||
// Should never fail!
|
// Should never fail!
|
||||||
let _ = new_attrs.insert(attr_name_normal, avas_normal);
|
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 {
|
pub fn compare(&self, rhs: &Entry<EntryValid, EntryNew>) -> bool {
|
||||||
self.attrs == rhs.attrs
|
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> {
|
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::Entry as ProtoEntry;
|
||||||
use super::proto_v1::{
|
use super::proto_v1::{
|
||||||
AuthRequest, AuthResponse, AuthStatus, CreateRequest, DeleteRequest, ModifyRequest, Response,
|
AuthRequest, AuthResponse, AuthStatus, CreateRequest, DeleteRequest, ModifyRequest, Response,
|
||||||
SearchRequest, SearchResponse
|
SearchRequest, SearchResponse, SearchRecycledRequest, ReviveRecycledRequest
|
||||||
};
|
};
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
use entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
||||||
|
@ -94,21 +94,21 @@ impl SearchEvent {
|
||||||
pub fn new_impersonate(filter: Filter<FilterInvalid>) -> Self {
|
pub fn new_impersonate(filter: Filter<FilterInvalid>) -> Self {
|
||||||
SearchEvent {
|
SearchEvent {
|
||||||
internal: false,
|
internal: false,
|
||||||
|
filter: filter,
|
||||||
|
class: (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_rec_request(request: SearchRecycledRequest) -> Self {
|
||||||
|
SearchEvent {
|
||||||
filter: Filter::And(vec![
|
filter: Filter::And(vec![
|
||||||
Filter::AndNot(Box::new(
|
Filter::Eq(
|
||||||
Filter::Or(vec![
|
"class".to_string(),
|
||||||
Filter::Eq(
|
"recycled".to_string(),
|
||||||
"class".to_string(),
|
),
|
||||||
"tombstone".to_string(),
|
Filter::from(&request.filter)
|
||||||
),
|
]),
|
||||||
Filter::Eq(
|
internal: false,
|
||||||
"class".to_string(),
|
|
||||||
"recycled".to_string(),
|
|
||||||
)
|
|
||||||
])
|
|
||||||
)),
|
|
||||||
filter
|
|
||||||
]),
|
|
||||||
class: (),
|
class: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,21 @@ impl Message for DeleteEvent {
|
||||||
impl DeleteEvent {
|
impl DeleteEvent {
|
||||||
pub fn from_request(request: DeleteRequest) -> Self {
|
pub fn from_request(request: DeleteRequest) -> Self {
|
||||||
DeleteEvent {
|
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,
|
internal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -309,3 +323,28 @@ impl PurgeEvent {
|
||||||
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 struct AuthResponse {
|
||||||
pub status: AuthStatus,
|
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 error::{OperationError, SchemaError};
|
||||||
use event::{
|
use event::{
|
||||||
AuthEvent, AuthResult, CreateEvent, DeleteEvent, ExistsEvent, ModifyEvent, OpResult,
|
AuthEvent, AuthResult, CreateEvent, DeleteEvent, ExistsEvent, ModifyEvent, OpResult,
|
||||||
SearchEvent, SearchResult, PurgeEvent,
|
SearchEvent, SearchResult, PurgeEvent, ReviveRecycledEvent
|
||||||
};
|
};
|
||||||
use filter::{Filter, FilterInvalid};
|
use filter::{Filter, FilterInvalid};
|
||||||
use log::EventLog;
|
use log::EventLog;
|
||||||
use modify::ModifyList;
|
use modify::{ModifyList, Modify};
|
||||||
use plugins::Plugins;
|
use plugins::Plugins;
|
||||||
use schema::{Schema, SchemaReadTransaction, SchemaTransaction, SchemaWriteTransaction};
|
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
|
// performing un-indexed searches on attr's that don't exist in the
|
||||||
// server. This is why ExtensibleObject can only take schema that
|
// server. This is why ExtensibleObject can only take schema that
|
||||||
// exists in the server, not arbitrary attr names.
|
// exists in the server, not arbitrary attr names.
|
||||||
|
audit_log!(au, "search: filter -> {:?}", se.filter);
|
||||||
|
|
||||||
// TODO: Normalise the filter
|
// TODO: Normalise the filter
|
||||||
|
|
||||||
|
@ -471,14 +472,75 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
res
|
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
|
// 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> {
|
// Should this take a revive event?
|
||||||
// Revive an entry to live.
|
pub fn revive_recycled(&self, au: &mut AuditScope, re: &ReviveRecycledEvent) -> Result<(), OperationError> {
|
||||||
unimplemented!()
|
// 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> {
|
pub fn modify(&self, au: &mut AuditScope, me: &ModifyEvent) -> Result<(), OperationError> {
|
||||||
|
@ -629,6 +691,19 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
res
|
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.
|
// internal server operation types.
|
||||||
// These just wrap the fn create/search etc, but they allow
|
// These just wrap the fn create/search etc, but they allow
|
||||||
// creating the needed create event with the correct internal flags
|
// 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::be::{Backend, BackendTransaction};
|
||||||
use super::super::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
use super::super::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid};
|
||||||
use super::super::error::OperationError;
|
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::filter::Filter;
|
||||||
use super::super::log;
|
use super::super::log;
|
||||||
use super::super::modify::{Modify, ModifyList};
|
use super::super::modify::{Modify, ModifyList};
|
||||||
use super::super::proto_v1::Entry as ProtoEntry;
|
use super::super::proto_v1::Entry as ProtoEntry;
|
||||||
use super::super::proto_v1::Filter as ProtoFilter;
|
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::Modify as ProtoModify;
|
||||||
use super::super::proto_v1::ModifyList as ProtoModifyList;
|
use super::super::proto_v1::ModifyList as ProtoModifyList;
|
||||||
use super::super::schema::Schema;
|
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