vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

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:
Amigrations/mysql/2020-04-09-235005_add_cipher_delete_date/down.sql | 1+
Amigrations/mysql/2020-04-09-235005_add_cipher_delete_date/up.sql | 3+++
Amigrations/postgresql/2020-04-09-235005_add_cipher_delete_date/down.sql | 1+
Amigrations/postgresql/2020-04-09-235005_add_cipher_delete_date/up.sql | 3+++
Amigrations/sqlite/2020-04-09-235005_add_cipher_delete_date/down.sql | 1+
Amigrations/sqlite/2020-04-09-235005_add_cipher_delete_date/up.sql | 3+++
Msrc/api/core/ciphers.rs | 131++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/db/models/cipher.rs | 3+++
Msrc/db/schemas/mysql/schema.rs | 1+
Msrc/db/schemas/postgresql/schema.rs | 1+
Msrc/db/schemas/sqlite/schema.rs | 1+
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>, } }