From ff828e4f4aaf31a6113933c09fa7ece0e59c368f Mon Sep 17 00:00:00 2001 From: Firstyear Date: Wed, 1 May 2019 14:06:22 +1000 Subject: [PATCH] Add DBVersioning for entries (#47) --- designs/auth.rst | 31 +++++ src/lib/be/dbentry.rs | 20 +++ src/lib/be/mod.rs | 285 ++++++++++++++++++++++---------------- src/lib/entry.rs | 90 ++++++++---- src/lib/error.rs | 5 +- src/lib/event.rs | 2 +- src/lib/plugins/refint.rs | 1 - src/lib/server.rs | 40 +----- 8 files changed, 283 insertions(+), 191 deletions(-) create mode 100644 src/lib/be/dbentry.rs diff --git a/designs/auth.rst b/designs/auth.rst index 8fa0f46e6..92a3a582c 100644 --- a/designs/auth.rst +++ b/designs/auth.rst @@ -296,3 +296,34 @@ system yet), method two probably is the best, but you need token constraint to make sure you can't replay to another host. + +Brain Dump Internal Details +=========================== + +Credentials should be a real struct on entry, that is serialised to str to dbentry. This allows repl +to still work, but then we can actually keep detailed structures for types in the DB instead. When +we send to proto entry, we could probably keep it as a real struct on protoentry, but then we could +eliminate all private types from transmission. + + +When we login, we need to know what groups/roles are relevant to that authentication. To achieve this +we can have each group contain a policy of auth types (the credentials above all provide an auth +type). The login then has a known auth type of "how" they logged in, so when we go to generate +the users "token" for that session, we can correlate these, and only attach groups that satisfy +the authentication type requirements. + +IE the session associates the method you used to login to your token and a cookie. + +If you require extra groups, then we should support a token refresh that given the prior auth + +extra factors, we can then re-issue the token to support the extra groups as presented. We may +also want some auth types to NOT allow refresh. + +We may want groups to support expiry where they are not valid past some time stamp. This may +required tagging or other details. + + +How do we ensure integrity of the token? Do we have to? Is the clients job to trust the token given +the TLS tunnel? + + + diff --git a/src/lib/be/dbentry.rs b/src/lib/be/dbentry.rs new file mode 100644 index 000000000..f51f3d5ad --- /dev/null +++ b/src/lib/be/dbentry.rs @@ -0,0 +1,20 @@ +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Debug)] +pub struct DbEntryV1 { + pub attrs: BTreeMap>, +} + +// REMEMBER: If you add a new version here, you MUST +// update entry.rs into_dbentry to export to the latest +// type always!! +#[derive(Serialize, Deserialize, Debug)] +pub enum DbEntryVers { + V1(DbEntryV1), +} + +// This is actually what we store into the DB. +#[derive(Serialize, Deserialize, Debug)] +pub struct DbEntry { + pub ent: DbEntryVers, +} diff --git a/src/lib/be/mod.rs b/src/lib/be/mod.rs index dc34327a7..0595b25e9 100644 --- a/src/lib/be/mod.rs +++ b/src/lib/be/mod.rs @@ -5,38 +5,28 @@ use r2d2_sqlite::SqliteConnectionManager; use rusqlite::types::ToSql; use rusqlite::NO_PARAMS; use serde_json; +use std::convert::TryFrom; use std::fs; // use uuid; use crate::audit::AuditScope; +use crate::be::dbentry::DbEntry; use crate::entry::{Entry, EntryCommitted, EntryNew, EntryValid}; use crate::error::{ConsistencyError, OperationError}; use crate::filter::{Filter, FilterValid}; +pub mod dbentry; mod idl; mod mem_be; mod sqlite_be; #[derive(Debug)] struct IdEntry { - // FIXME: This should be u64, but sqlite uses i64 ... + // TODO: for now this is i64 to make sqlite work, but entry is u64 for indexing reasons! id: i64, data: String, } -/* -pub enum BackendType { - Memory, // isn't memory just sqlite with file :memory: ? - SQLite, -} -*/ - -#[derive(Debug, PartialEq)] -pub enum BackendError { - EmptyRequest, - EntryMissingId, -} - pub struct Backend { pool: Pool, } @@ -59,7 +49,7 @@ pub trait BackendReadTransaction { &self, au: &mut AuditScope, filt: &Filter, - ) -> Result>, BackendError> { + ) -> Result>, OperationError> { // Do things // Alloc a vec for the entries. // FIXME: Make this actually a good size for the result set ... @@ -99,13 +89,17 @@ pub trait BackendReadTransaction { } // Do other things // Now, de-serialise the raw_entries back to entries, and populate their ID's + let entries: Vec> = raw_entries .iter() - .filter_map(|id_ent| { - // TODO: Should we do better than unwrap? - let mut e: Entry = - serde_json::from_str(id_ent.data.as_str()).unwrap(); - e.id = Some(id_ent.id); + .map(|id_ent| { + // TODO: Should we do better than unwrap? And if so, how? + // we need to map this error, so probably need to collect to Result, _> + // and then to try_audit! on that. + let db_e = serde_json::from_str(id_ent.data.as_str()).unwrap(); + Entry::from_dbentry(db_e, u64::try_from(id_ent.id).unwrap()) + }) + .filter_map(|e| { if e.entry_match_no_index(&filt) { Some(e) } else { @@ -126,7 +120,7 @@ pub trait BackendReadTransaction { &self, au: &mut AuditScope, filt: &Filter, - ) -> Result { + ) -> Result { // Do a final optimise of the filter // At the moment, technically search will do this, but it won't always be the // case once this becomes a standalone function. @@ -236,14 +230,65 @@ impl BackendWriteTransaction { } } - fn internal_create( + fn internal_create( &self, au: &mut AuditScope, - entries: &Vec>, + dbentries: &Vec, ) -> Result<(), OperationError> { - audit_segment!(au, || { - // Start be audit timer + // Get the max id from the db. We store this ourselves to avoid max() calls. + let mut id_max = self.get_id2entry_max_id(); + let ser_entries: Vec = dbentries + .iter() + .map(|ser_db_e| { + id_max = id_max + 1; + + IdEntry { + id: id_max, + // TODO: Should we do better than unwrap? + data: serde_json::to_string(&ser_db_e).unwrap(), + } + }) + .collect(); + + audit_log!(au, "serialising: {:?}", ser_entries); + + // THIS IS PROBABLY THE BIT WHERE YOU NEED DB ABSTRACTION + { + let mut stmt = try_audit!( + au, + self.conn + .prepare("INSERT INTO id2entry (id, data) VALUES (:id, :data)"), + "rusqlite error {:?}", + OperationError::SQLiteError + ); + + // write them all + for ser_entry in ser_entries { + // TODO: Prepared statement. + try_audit!( + au, + stmt.execute_named(&[ + (":id", &ser_entry.id as &ToSql), + (":data", &ser_entry.data as &ToSql) + ]), + "rusqlite error {:?}", + OperationError::SQLiteError + ); + } + } + + Ok(()) + } + + pub fn create( + &self, + au: &mut AuditScope, + entries: &Vec>, + ) -> Result<(), OperationError> { + // figured we would want a audit_segment to wrap internal_create so when doing profiling we can + // tell which function is calling it. either this one or restore. + audit_segment!(au, || { if entries.is_empty() { // TODO: Better error // End the timer @@ -254,111 +299,80 @@ impl BackendWriteTransaction { // we do this outside the txn to avoid blocking needlessly. // However, it could be pointless due to the extra string allocs ... - // Get the max id from the db. We store this ourselves to avoid max(). - let mut id_max = self.get_id2entry_max_id(); + let dbentries: Vec<_> = entries.iter().map(|e| e.into_dbentry()).collect(); - let ser_entries: Vec = entries - .iter() - .map(|val| { - // TODO: Should we do better than unwrap? - id_max = id_max + 1; - IdEntry { - id: id_max, - data: serde_json::to_string(&val).unwrap(), - } - }) - .collect(); + self.internal_create(au, &dbentries) - audit_log!(au, "serialising: {:?}", ser_entries); - - // THIS IS PROBABLY THE BIT WHERE YOU NEED DB ABSTRACTION - { - let mut stmt = try_audit!( - au, - self.conn - .prepare("INSERT INTO id2entry (id, data) VALUES (:id, :data)"), - "rusqlite error {:?}", - OperationError::SQLiteError - ); - - // write them all - for ser_entry in ser_entries { - // TODO: Prepared statement. - try_audit!( - au, - stmt.execute_named(&[ - (":id", &ser_entry.id as &ToSql), - (":data", &ser_entry.data as &ToSql) - ]), - "rusqlite error {:?}", - OperationError::SQLiteError - ); - } - - // TODO: update indexes (as needed) - } - - Ok(()) + // TODO: update indexes (as needed) }) } - pub fn create( - &self, - au: &mut AuditScope, - entries: &Vec>, - ) -> Result<(), OperationError> { - // figured we would want a audit_segment to wrap internal_create so when doing profiling we can - // tell which function is calling it. either this one or restore. - audit_segment!(au, || self.internal_create(au, entries)) - } - pub fn modify( &self, au: &mut AuditScope, entries: &Vec>, - ) -> Result<(), BackendError> { + ) -> Result<(), OperationError> { if entries.is_empty() { - // TODO: Better error - return Err(BackendError::EmptyRequest); + return Err(OperationError::EmptyRequest); } // Assert the Id's exist on the entry, and serialise them. - let ser_entries: Vec = entries + // Now, that means the ID must be > 0!!! + let ser_entries: Result, _> = entries .iter() - .filter_map(|e| { - match e.id { - Some(id) => { - Some(IdEntry { - id: id, - // TODO: Should we do better than unwrap? - data: serde_json::to_string(&e).unwrap(), - }) - } - None => None, - } + .map(|e| { + let db_e = e.into_dbentry(); + + let id = i64::try_from(e.get_id()) + .map_err(|_| OperationError::InvalidEntryID) + .and_then(|id| { + if id == 0 { + Err(OperationError::InvalidEntryID) + } else { + Ok(id) + } + })?; + + let data = + serde_json::to_string(&db_e).map_err(|_| OperationError::SerdeJsonError)?; + + Ok(IdEntry { + // TODO: Instead of getting these from the entry, we could lookup + // uuid -> id in the index. + id: id, + data: data, + }) }) .collect(); + let ser_entries = try_audit!(au, ser_entries); + audit_log!(au, "serialising: {:?}", ser_entries); // Simple: If the list of id's is not the same as the input list, we are missing id's // TODO: This check won't be needed once I rebuild the entry state types. if entries.len() != ser_entries.len() { - return Err(BackendError::EntryMissingId); + return Err(OperationError::InvalidEntryState); } // Now, given the list of id's, update them { - // TODO: ACTUALLY HANDLE THIS ERROR WILLIAM YOU LAZY SHIT. - let mut stmt = self - .conn - .prepare("UPDATE id2entry SET data = :data WHERE id = :id") - .unwrap(); + let mut stmt = try_audit!( + au, + self.conn + .prepare("UPDATE id2entry SET data = :data WHERE id = :id"), + "RusqliteError: {:?}", + OperationError::SQLiteError + ); - ser_entries.iter().for_each(|ser_ent| { - stmt.execute_named(&[(":id", &ser_ent.id), (":data", &ser_ent.data)]) - .unwrap(); - }); + for ser_ent in ser_entries.iter() { + try_audit!( + au, + stmt.execute_named(&[(":id", &ser_ent.id), (":data", &ser_ent.data)]), + "RusqliteError: {:?}", + OperationError::SQLiteError + ); + } } Ok(()) @@ -368,21 +382,36 @@ impl BackendWriteTransaction { &self, au: &mut AuditScope, entries: &Vec>, - ) -> Result<(), BackendError> { + ) -> Result<(), OperationError> { // Perform a search for the entries --> This is a problem for the caller audit_segment!(au, || { if entries.is_empty() { // TODO: Better error - return Err(BackendError::EmptyRequest); + return Err(OperationError::EmptyRequest); } // Assert the Id's exist on the entry. - let id_list: Vec = entries.iter().filter_map(|entry| entry.id).collect(); + let id_list: Result, _> = entries + .iter() + .map(|e| { + i64::try_from(e.get_id()) + .map_err(|_| OperationError::InvalidEntryID) + .and_then(|id| { + if id == 0 { + Err(OperationError::InvalidEntryID) + } else { + Ok(id) + } + }) + }) + .collect(); + + let id_list = try_audit!(au, id_list); // Simple: If the list of id's is not the same as the input list, we are missing id's // TODO: This check won't be needed once I rebuild the entry state types. if entries.len() != id_list.len() { - return Err(BackendError::EntryMissingId); + return Err(OperationError::InvalidEntryState); } // Now, given the list of id's, delete them. @@ -429,14 +458,9 @@ impl BackendWriteTransaction { } } - let entries: Vec> = raw_entries + let entries: Vec = raw_entries .iter() - .filter_map(|id_ent| { - let mut e: Entry = - serde_json::from_str(id_ent.data.as_str()).unwrap(); - e.id = Some(id_ent.id); - Some(e) - }) + .map(|id_ent| serde_json::from_str(id_ent.data.as_str()).unwrap()) .collect(); let mut serializedEntries = serde_json::to_string_pretty(&entries); @@ -488,7 +512,7 @@ impl BackendWriteTransaction { self.purge(audit); } - let entriesOption: Result>, serde_json::Error> = + let entriesOption: Result, serde_json::Error> = serde_json::from_str(&serializedString); let entries = try_audit!( @@ -500,8 +524,9 @@ impl BackendWriteTransaction { self.internal_create(audit, &entries) - // run re-index after db is restored - // run db verify + // TODO: run re-index after db is restored + // TODO; run db verify + // self.verify(audit) } pub fn commit(mut self) -> Result<(), OperationError> { @@ -664,9 +689,7 @@ mod tests { use super::super::audit::AuditScope; use super::super::entry::{Entry, EntryInvalid, EntryNew}; use super::super::filter::Filter; - use super::{ - Backend, BackendError, BackendReadTransaction, BackendWriteTransaction, OperationError, - }; + use super::{Backend, BackendReadTransaction, BackendWriteTransaction, OperationError}; macro_rules! run_test { ($test_fn:expr) => {{ @@ -729,9 +752,27 @@ mod tests { #[test] fn test_simple_search() { - run_test!(|audit: &mut AuditScope, _be: &BackendWriteTransaction| { + run_test!(|audit: &mut AuditScope, be: &BackendWriteTransaction| { audit_log!(audit, "Simple Search"); - unimplemented!(); + + let mut e: Entry = Entry::new(); + e.add_ava(String::from("userid"), String::from("claire")); + let e = unsafe { e.to_valid_new() }; + + let single_result = be.create(audit, &vec![e.clone()]); + assert!(single_result.is_ok()); + // Test a simple EQ search + + let filt = Filter::Eq("userid".to_string(), "claire".to_string()); + + let r = be.search(audit, &filt); + assert!(r.expect("Search failed!").len() == 1); + + // Test empty search + + // Test class pres + + // Search with no results }); } diff --git a/src/lib/entry.rs b/src/lib/entry.rs index a23017b9b..90fc2e5b3 100644 --- a/src/lib/entry.rs +++ b/src/lib/entry.rs @@ -6,6 +6,9 @@ use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid}; use crate::proto_v1::Entry as ProtoEntry; use crate::schema::{SchemaAttribute, SchemaClass, SchemaReadTransaction}; use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction}; + +use crate::be::dbentry::{DbEntry, DbEntryV1, DbEntryVers}; + use std::collections::btree_map::{Iter as BTreeIter, IterMut as BTreeIterMut}; use std::collections::BTreeMap; use std::collections::HashMap; @@ -120,24 +123,23 @@ impl<'a> Iterator for EntryAvasMut<'a> { // This is specifically important for the commit to the backend, as we only want to // commit validated types. -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +#[derive(Clone, Copy, Debug, Deserialize)] pub struct EntryNew; // new -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] -pub struct EntryCommitted; // It's been in the DB, so it has an id - // pub struct EntryPurged; +#[derive(Clone, Copy, Debug, Deserialize)] +pub struct EntryCommitted { + id: u64, +} // It's been in the DB, so it has an id + // pub struct EntryPurged; -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +#[derive(Clone, Copy, Debug, Deserialize)] pub struct EntryValid; // Asserted with schema. -#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +#[derive(Clone, Copy, Debug, Deserialize)] pub struct EntryInvalid; // Modified -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug, Deserialize)] pub struct Entry { valid: VALID, state: STATE, - pub id: Option, - // Flag if we have been schema checked or not. - // pub schema_validated: bool, attrs: BTreeMap>, } @@ -148,13 +150,12 @@ impl Entry { // This means NEVER COMMITED valid: EntryInvalid, state: EntryNew, - id: None, attrs: BTreeMap::new(), } } // FIXME: Can we consume protoentry? - pub fn from( + pub fn from_proto_entry( audit: &mut AuditScope, e: &ProtoEntry, qs: &QueryServerWriteTransaction, @@ -192,7 +193,6 @@ impl Entry { // sets so that BST works. state: EntryNew, valid: EntryInvalid, - id: None, attrs: x, }) } @@ -208,7 +208,6 @@ impl Entry { let Entry { valid: _, state, - id, attrs, } = self; @@ -253,7 +252,6 @@ impl Entry { let ne = Entry { valid: EntryValid, state: state, - id: id, attrs: new_attrs, }; // Now validate. @@ -359,7 +357,6 @@ where Entry { valid: self.valid, state: self.state, - id: self.id, attrs: self.attrs.clone(), } } @@ -375,22 +372,32 @@ impl Entry { Entry { valid: EntryValid, state: EntryNew, - id: self.id, attrs: self.attrs, } } +} +// Both invalid states can be reached from "entry -> invalidate" +impl Entry { #[cfg(test)] pub unsafe fn to_valid_committed(self) -> Entry { Entry { valid: EntryValid, - state: EntryCommitted, - id: self.id, + state: EntryCommitted { id: 0 }, attrs: self.attrs, } } +} - // Both invalid states can be reached from "entry -> invalidate" +impl Entry { + #[cfg(test)] + pub unsafe fn to_valid_committed(self) -> Entry { + Entry { + valid: EntryValid, + state: self.state, + attrs: self.attrs, + } + } } impl Entry { @@ -418,23 +425,48 @@ impl Entry { Entry { valid: EntryValid, - state: EntryCommitted, - id: self.id, + state: self.state, attrs: attrs_new, } } - pub fn get_id(&self) -> i64 { - self.id.expect("ID corrupted!?!?") + pub fn get_id(&self) -> u64 { + self.state.id + } + + pub fn from_dbentry(db_e: DbEntry, id: u64) -> Self { + Entry { + valid: EntryValid, + state: EntryCommitted { id }, + attrs: match db_e.ent { + DbEntryVers::V1(v1) => v1.attrs, + }, + } } } impl Entry { + // Returns the entry in the latest DbEntry format we are aware of. + pub fn into_dbentry(&self) -> DbEntry { + // In the future this will do extra work to process uuid + // into "attributes" suitable for dbentry storage. + + // How will this work with replication? + // + // Alternately, we may have higher-level types that translate entry + // into proper structures, and they themself emit/modify entries? + + DbEntry { + ent: DbEntryVers::V1(DbEntryV1 { + attrs: self.attrs.clone(), + }), + } + } + pub fn invalidate(self) -> Entry { Entry { valid: EntryInvalid, state: self.state, - id: self.id, attrs: self.attrs, } } @@ -442,8 +474,9 @@ impl Entry { pub fn seal(self) -> Entry { Entry { valid: self.valid, - state: EntryCommitted, - id: self.id, + state: EntryCommitted { + id: unimplemented!(), + }, attrs: self.attrs, } } @@ -699,7 +732,6 @@ where let mut ne: Entry = Entry { valid: self.valid, state: self.state, - id: self.id, attrs: self.attrs.clone(), }; @@ -769,8 +801,6 @@ mod tests { let mut e: Entry = Entry::new(); e.add_ava(String::from("userid"), String::from("william")); - - let _d = serde_json::to_string_pretty(&e).unwrap(); } #[test] diff --git a/src/lib/error.rs b/src/lib/error.rs index 4dd79a5de..9222775e4 100644 --- a/src/lib/error.rs +++ b/src/lib/error.rs @@ -23,6 +23,7 @@ pub enum OperationError { Plugin, FilterGeneration, InvalidDBState, + InvalidEntryID, InvalidRequestState, InvalidState, InvalidEntryState, @@ -38,8 +39,8 @@ pub enum ConsistencyError { // Class, Attribute SchemaClassMissingAttribute(String, String), QueryServerSearchFailure, - EntryUuidCorrupt(i64), + EntryUuidCorrupt(u64), UuidIndexCorrupt(String), UuidNotUnique(String), - RefintNotUpheld(i64), + RefintNotUpheld(u64), } diff --git a/src/lib/event.rs b/src/lib/event.rs index a7db4d4ef..6a40ffe1f 100644 --- a/src/lib/event.rs +++ b/src/lib/event.rs @@ -156,7 +156,7 @@ impl CreateEvent { let rentries: Result, _> = request .entries .iter() - .map(|e| Entry::from(audit, e, qs)) + .map(|e| Entry::from_proto_entry(audit, e, qs)) .collect(); match rentries { Ok(entries) => Ok(CreateEvent { diff --git a/src/lib/plugins/refint.rs b/src/lib/plugins/refint.rs index e38393eea..e091532b8 100644 --- a/src/lib/plugins/refint.rs +++ b/src/lib/plugins/refint.rs @@ -53,7 +53,6 @@ impl Plugin for ReferentialIntegrity { "referential_integrity" } - // Why are these checks all in post? // // There is a situation to account for which is that a create or mod diff --git a/src/lib/server.rs b/src/lib/server.rs index e6c9a70e6..ff4f16da7 100644 --- a/src/lib/server.rs +++ b/src/lib/server.rs @@ -4,9 +4,7 @@ use std::sync::Arc; use crate::audit::AuditScope; -use crate::be::{ - Backend, BackendError, BackendReadTransaction, BackendTransaction, BackendWriteTransaction, -}; +use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction}; use crate::constants::{JSON_ANONYMOUS_V1, JSON_SYSTEM_INFO_V1}; use crate::entry::{Entry, EntryCommitted, EntryInvalid, EntryNew, EntryValid}; @@ -106,7 +104,6 @@ pub trait QueryServerReadTransaction { // How to get schema? let vf = match ee.filter.validate(self.get_schema()) { Ok(f) => f, - // TODO: Do something with this error Err(e) => return Err(OperationError::SchemaViolation(e)), }; @@ -649,15 +646,7 @@ impl<'a> QueryServerWriteTransaction<'a> { let mut audit_be = AuditScope::new("backend_modify"); - let res = self - .be_txn - // Change this to an update, not delete. - .modify(&mut audit_be, &del_cand) - .map(|_| ()) - .map_err(|e| match e { - BackendError::EmptyRequest => OperationError::EmptyRequest, - BackendError::EntryMissingId => OperationError::InvalidRequestState, - }); + let res = self.be_txn.modify(&mut audit_be, &del_cand); au.append_scope(audit_be); if res.is_err() { @@ -700,12 +689,7 @@ impl<'a> QueryServerWriteTransaction<'a> { let res = self .be_txn // Change this to an update, not delete. - .delete(&mut audit_be, &ts) - .map(|_| ()) - .map_err(|e| match e { - BackendError::EmptyRequest => OperationError::EmptyRequest, - BackendError::EntryMissingId => OperationError::InvalidRequestState, - }); + .delete(&mut audit_be, &ts); au.append_scope(audit_be); if res.is_err() { @@ -735,14 +719,7 @@ impl<'a> QueryServerWriteTransaction<'a> { // 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, - }); + let res = self.be_txn.modify(&mut audit_be, &tombstone_cand); au.append_scope(audit_be); if res.is_err() { @@ -871,14 +848,7 @@ impl<'a> QueryServerWriteTransaction<'a> { // Backend Modify let mut audit_be = AuditScope::new("backend_modify"); - let res = self - .be_txn - .modify(&mut audit_be, &norm_cand) - .map(|_| ()) - .map_err(|e| match e { - BackendError::EmptyRequest => OperationError::EmptyRequest, - BackendError::EntryMissingId => OperationError::InvalidRequestState, - }); + let res = self.be_txn.modify(&mut audit_be, &norm_cand); au.append_scope(audit_be); if res.is_err() {