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 new file mode 100644 index 00000000..8b137891 --- /dev/null +++ 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 new file mode 100644 index 00000000..ad08ab7f --- /dev/null +++ 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 new file mode 100644 index 00000000..8b137891 --- /dev/null +++ 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 new file mode 100644 index 00000000..1a2edde4 --- /dev/null +++ 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 new file mode 100644 index 00000000..8b137891 --- /dev/null +++ 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 new file mode 100644 index 00000000..ad08ab7f --- /dev/null +++ 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 index dcfd3f98..482bfe6f 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -49,10 +49,16 @@ pub fn routes() -> Vec { 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//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//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//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//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/")] 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//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 = "")] fn delete_cipher_selected(data: JsonUpcase, 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(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, &nt) { - return error; - }; - } - - Ok(()) + _delete_multiple_ciphers(data, headers, conn, false, nt) } #[post("/ciphers/delete", data = "")] fn delete_cipher_selected_post(data: JsonUpcase, 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 = "")] +fn delete_cipher_selected_put(data: JsonUpcase, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { + _delete_multiple_ciphers(data, headers, conn, true, nt) +} + +#[put("/ciphers//restore")] +fn restore_cipher_put(uuid: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { + _restore_cipher_by_uuid(&uuid, &headers, &conn, &nt) +} + +#[put("/ciphers//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 = "")] +fn restore_cipher_selected(data: JsonUpcase, 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, 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, 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 index 10d96b33..a6ce5044 100644 --- 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, + pub deleted_at: Option, } /// 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 index c03f6a5d..42943689 100644 --- 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, + deleted_at -> Nullable, } } diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index b5d0a6ec..adcbbd3c 100644 --- 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, + deleted_at -> Nullable, } } diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index b5d0a6ec..adcbbd3c 100644 --- 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, + deleted_at -> Nullable, } }