Initial support for soft deletes

This commit is contained in:
Daniel García 2020-04-17 22:35:27 +02:00
parent 5a390a973f
commit e3b00b59a7
No known key found for this signature in database
GPG Key ID: FC8A7D14C3CD543A
11 changed files with 126 additions and 25 deletions

View File

@ -0,0 +1,3 @@
ALTER TABLE ciphers
ADD COLUMN
deleted_at DATETIME;

View File

@ -0,0 +1,3 @@
ALTER TABLE ciphers
ADD COLUMN
deleted_at TIMESTAMP;

View File

@ -0,0 +1,3 @@
ALTER TABLE ciphers
ADD COLUMN
deleted_at DATETIME;

View File

@ -49,10 +49,16 @@ pub fn routes() -> Vec<Route> {
put_cipher, put_cipher,
delete_cipher_post, delete_cipher_post,
delete_cipher_post_admin, delete_cipher_post_admin,
delete_cipher_put,
delete_cipher_put_admin,
delete_cipher, delete_cipher,
delete_cipher_admin, delete_cipher_admin,
delete_cipher_selected, delete_cipher_selected,
delete_cipher_selected_post, delete_cipher_selected_post,
delete_cipher_selected_put,
restore_cipher_put,
restore_cipher_put_admin,
restore_cipher_selected,
delete_all, delete_all,
move_cipher_selected, move_cipher_selected,
move_cipher_selected_put, move_cipher_selected_put,
@ -819,48 +825,62 @@ fn delete_attachment_admin(
#[post("/ciphers/<uuid>/delete")] #[post("/ciphers/<uuid>/delete")]
fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt) _delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
} }
#[post("/ciphers/<uuid>/delete-admin")] #[post("/ciphers/<uuid>/delete-admin")]
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt) _delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
}
#[put("/ciphers/<uuid>/delete")]
fn delete_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, true, &nt)
}
#[put("/ciphers/<uuid>/delete-admin")]
fn delete_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, true, &nt)
} }
#[delete("/ciphers/<uuid>")] #[delete("/ciphers/<uuid>")]
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { fn delete_cipher(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt) _delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
} }
#[delete("/ciphers/<uuid>/admin")] #[delete("/ciphers/<uuid>/admin")]
fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { fn delete_cipher_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn, &nt) _delete_cipher_by_uuid(&uuid, &headers, &conn, false, &nt)
} }
#[delete("/ciphers", data = "<data>")] #[delete("/ciphers", data = "<data>")]
fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { fn delete_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data: Value = data.into_inner().data; _delete_multiple_ciphers(data, headers, conn, false, nt)
let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array"),
},
None => err!("Request missing ids field"),
};
for uuid in uuids {
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &nt) {
return error;
};
}
Ok(())
} }
#[post("/ciphers/delete", data = "<data>")] #[post("/ciphers/delete", data = "<data>")]
fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
delete_cipher_selected(data, headers, conn, nt) _delete_multiple_ciphers(data, headers, conn, false, nt)
}
#[put("/ciphers/delete", data = "<data>")]
fn delete_cipher_selected_put(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_delete_multiple_ciphers(data, headers, conn, true, nt)
}
#[put("/ciphers/<uuid>/restore")]
fn restore_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
}
#[put("/ciphers/<uuid>/restore-admin")]
fn restore_cipher_put_admin(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_restore_cipher_by_uuid(&uuid, &headers, &conn, &nt)
}
#[put("/ciphers/restore", data = "<data>")]
fn restore_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
_restore_multiple_ciphers(data, headers, conn, nt)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -974,8 +994,8 @@ fn delete_all(
} }
} }
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult { fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_delete: bool, nt: &Notify) -> EmptyResult {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) { let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher, Some(cipher) => cipher,
None => err!("Cipher doesn't exist"), None => err!("Cipher doesn't exist"),
}; };
@ -984,11 +1004,74 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Not
err!("Cipher can't be deleted by user") err!("Cipher can't be deleted by user")
} }
cipher.delete(&conn)?; if soft_delete {
cipher.deleted_at = Some(chrono::Utc::now().naive_utc());
cipher.save(&conn)?;
} else {
cipher.delete(&conn)?;
}
nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn)); nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(()) Ok(())
} }
fn _delete_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, soft_delete: bool, nt: Notify) -> EmptyResult {
let data: Value = data.into_inner().data;
let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array"),
},
None => err!("Request missing ids field"),
};
for uuid in uuids {
if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, soft_delete, &nt) {
return error;
};
}
Ok(())
}
fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist"),
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
err!("Cipher can't be restored by user")
}
cipher.deleted_at = None;
cipher.save(&conn)?;
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
Ok(())
}
fn _restore_multiple_ciphers(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
let data: Value = data.into_inner().data;
let uuids = match data.get("Ids") {
Some(ids) => match ids.as_array() {
Some(ids) => ids.iter().filter_map(Value::as_str),
None => err!("Posted ids field is not an array"),
},
None => err!("Request missing ids field"),
};
for uuid in uuids {
if let error @ Err(_) = _restore_cipher_by_uuid(uuid, &headers, &conn, &nt) {
return error;
};
}
Ok(())
}
fn _delete_cipher_attachment_by_id( fn _delete_cipher_attachment_by_id(
uuid: &str, uuid: &str,
attachment_id: &str, attachment_id: &str,

View File

@ -34,6 +34,7 @@ pub struct Cipher {
pub favorite: bool, pub favorite: bool,
pub password_history: Option<String>, pub password_history: Option<String>,
pub deleted_at: Option<NaiveDateTime>,
} }
/// Local methods /// Local methods
@ -58,6 +59,7 @@ impl Cipher {
data: String::new(), data: String::new(),
password_history: None, password_history: None,
deleted_at: None,
} }
} }
} }
@ -108,6 +110,7 @@ impl Cipher {
"Id": self.uuid, "Id": self.uuid,
"Type": self.atype, "Type": self.atype,
"RevisionDate": format_date(&self.updated_at), "RevisionDate": format_date(&self.updated_at),
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
"FolderId": self.get_folder_uuid(&user_uuid, &conn), "FolderId": self.get_folder_uuid(&user_uuid, &conn),
"Favorite": self.favorite, "Favorite": self.favorite,
"OrganizationId": self.organization_uuid, "OrganizationId": self.organization_uuid,

View File

@ -22,6 +22,7 @@ table! {
data -> Text, data -> Text,
favorite -> Bool, favorite -> Bool,
password_history -> Nullable<Text>, password_history -> Nullable<Text>,
deleted_at -> Nullable<Datetime>,
} }
} }

View File

@ -22,6 +22,7 @@ table! {
data -> Text, data -> Text,
favorite -> Bool, favorite -> Bool,
password_history -> Nullable<Text>, password_history -> Nullable<Text>,
deleted_at -> Nullable<Timestamp>,
} }
} }

View File

@ -22,6 +22,7 @@ table! {
data -> Text, data -> Text,
favorite -> Bool, favorite -> Bool,
password_history -> Nullable<Text>, password_history -> Nullable<Text>,
deleted_at -> Nullable<Timestamp>,
} }
} }