commit e3b00b59a7db760848f6b357fc4328081574aeac
parent 5a390a973ffcc5d7c75a18c441c554f8c6a54010
Author: Daniel GarcĂa <dani-garcia@users.noreply.github.com>
Date: Fri, 17 Apr 2020 22:35:27 +0200
Initial support for soft deletes
Diffstat:
11 files changed, 125 insertions(+), 24 deletions(-)
diff --git a/migrations/mysql/2020-04-09-235005_add_cipher_delete_date/down.sql b/migrations/mysql/2020-04-09-235005_add_cipher_delete_date/down.sql
@@ -0,0 +1 @@
+
diff --git a/migrations/mysql/2020-04-09-235005_add_cipher_delete_date/up.sql b/migrations/mysql/2020-04-09-235005_add_cipher_delete_date/up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE ciphers
+ ADD COLUMN
+ deleted_at DATETIME;
diff --git a/migrations/postgresql/2020-04-09-235005_add_cipher_delete_date/down.sql b/migrations/postgresql/2020-04-09-235005_add_cipher_delete_date/down.sql
@@ -0,0 +1 @@
+
diff --git a/migrations/postgresql/2020-04-09-235005_add_cipher_delete_date/up.sql b/migrations/postgresql/2020-04-09-235005_add_cipher_delete_date/up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE ciphers
+ ADD COLUMN
+ deleted_at TIMESTAMP;
diff --git a/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/down.sql b/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/down.sql
@@ -0,0 +1 @@
+
diff --git a/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/up.sql b/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE ciphers
+ ADD COLUMN
+ deleted_at DATETIME;
diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs
@@ -49,10 +49,16 @@ pub fn routes() -> Vec<Route> {
put_cipher,
delete_cipher_post,
delete_cipher_post_admin,
+ delete_cipher_put,
+ delete_cipher_put_admin,
delete_cipher,
delete_cipher_admin,
delete_cipher_selected,
delete_cipher_selected_post,
+ delete_cipher_selected_put,
+ restore_cipher_put,
+ restore_cipher_put_admin,
+ restore_cipher_selected,
delete_all,
move_cipher_selected,
move_cipher_selected_put,
@@ -819,48 +825,62 @@ fn delete_attachment_admin(
#[post("/ciphers/<uuid>/delete")]
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")]
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>")]
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")]
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>")]
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"),
- };
+#[post("/ciphers/delete", data = "<data>")]
+fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
+ _delete_multiple_ciphers(data, headers, conn, false, nt)
+}
- for uuid in uuids {
- if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &nt) {
- return error;
- };
- }
+#[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)
+}
- Ok(())
+#[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)
}
-#[post("/ciphers/delete", data = "<data>")]
-fn delete_cipher_selected_post(data: JsonUpcase<Value>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
- delete_cipher_selected(data, 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)]
@@ -974,8 +994,8 @@ fn delete_all(
}
}
-fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
- let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
+fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_delete: bool, nt: &Notify) -> EmptyResult {
+ let mut cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
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")
}
- 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));
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(
uuid: &str,
attachment_id: &str,
diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs
@@ -34,6 +34,7 @@ pub struct Cipher {
pub favorite: bool,
pub password_history: Option<String>,
+ pub deleted_at: Option<NaiveDateTime>,
}
/// Local methods
@@ -58,6 +59,7 @@ impl Cipher {
data: String::new(),
password_history: None,
+ deleted_at: None,
}
}
}
@@ -108,6 +110,7 @@ impl Cipher {
"Id": self.uuid,
"Type": self.atype,
"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),
"Favorite": self.favorite,
"OrganizationId": self.organization_uuid,
diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs
@@ -22,6 +22,7 @@ table! {
data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>,
+ deleted_at -> Nullable<Datetime>,
}
}
diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs
@@ -22,6 +22,7 @@ table! {
data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>,
+ deleted_at -> Nullable<Timestamp>,
}
}
diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs
@@ -22,6 +22,7 @@ table! {
data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>,
+ deleted_at -> Nullable<Timestamp>,
}
}