Merge pull request #1106 from jjlin/favorites

Track favorites on a per-user basis
pull/1115/head
Daniel García 4 years ago committed by GitHub
commit 19889187a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,13 @@
ALTER TABLE ciphers
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE;
-- Transfer favorite status for user-owned ciphers.
UPDATE ciphers
SET favorite = TRUE
WHERE EXISTS (
SELECT * FROM favorites
WHERE favorites.user_uuid = ciphers.user_uuid
AND favorites.cipher_uuid = ciphers.uuid
);
DROP TABLE favorites;

@ -0,0 +1,16 @@
CREATE TABLE favorites (
user_uuid CHAR(36) NOT NULL REFERENCES users(uuid),
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers(uuid),
PRIMARY KEY (user_uuid, cipher_uuid)
);
-- Transfer favorite status for user-owned ciphers.
INSERT INTO favorites(user_uuid, cipher_uuid)
SELECT user_uuid, uuid
FROM ciphers
WHERE favorite = TRUE
AND user_uuid IS NOT NULL;
ALTER TABLE ciphers
DROP COLUMN favorite;

@ -0,0 +1,13 @@
ALTER TABLE ciphers
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT FALSE;
-- Transfer favorite status for user-owned ciphers.
UPDATE ciphers
SET favorite = TRUE
WHERE EXISTS (
SELECT * FROM favorites
WHERE favorites.user_uuid = ciphers.user_uuid
AND favorites.cipher_uuid = ciphers.uuid
);
DROP TABLE favorites;

@ -0,0 +1,16 @@
CREATE TABLE favorites (
user_uuid VARCHAR(40) NOT NULL REFERENCES users(uuid),
cipher_uuid VARCHAR(40) NOT NULL REFERENCES ciphers(uuid),
PRIMARY KEY (user_uuid, cipher_uuid)
);
-- Transfer favorite status for user-owned ciphers.
INSERT INTO favorites(user_uuid, cipher_uuid)
SELECT user_uuid, uuid
FROM ciphers
WHERE favorite = TRUE
AND user_uuid IS NOT NULL;
ALTER TABLE ciphers
DROP COLUMN favorite;

@ -0,0 +1,13 @@
ALTER TABLE ciphers
ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT 0; -- FALSE
-- Transfer favorite status for user-owned ciphers.
UPDATE ciphers
SET favorite = 1
WHERE EXISTS (
SELECT * FROM favorites
WHERE favorites.user_uuid = ciphers.user_uuid
AND favorites.cipher_uuid = ciphers.uuid
);
DROP TABLE favorites;

@ -0,0 +1,71 @@
CREATE TABLE favorites (
user_uuid TEXT NOT NULL REFERENCES users(uuid),
cipher_uuid TEXT NOT NULL REFERENCES ciphers(uuid),
PRIMARY KEY (user_uuid, cipher_uuid)
);
-- Transfer favorite status for user-owned ciphers.
INSERT INTO favorites(user_uuid, cipher_uuid)
SELECT user_uuid, uuid
FROM ciphers
WHERE favorite = 1
AND user_uuid IS NOT NULL;
-- Drop the `favorite` column from the `ciphers` table, using the 12-step
-- procedure from <https://www.sqlite.org/lang_altertable.html#altertabrename>.
-- Note that some steps aren't applicable and are omitted.
-- 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF.
--
-- Diesel runs each migration in its own transaction. `PRAGMA foreign_keys`
-- is a no-op within a transaction, so this step must be done outside of this
-- file, before starting the Diesel migrations.
-- 2. Start a transaction.
--
-- Diesel already runs each migration in its own transaction.
-- 4. Use CREATE TABLE to construct a new table "new_X" that is in the
-- desired revised format of table X. Make sure that the name "new_X" does
-- not collide with any existing table name, of course.
CREATE TABLE new_ciphers(
uuid TEXT NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid TEXT REFERENCES users(uuid),
organization_uuid TEXT REFERENCES organizations(uuid),
atype INTEGER NOT NULL,
name TEXT NOT NULL,
notes TEXT,
fields TEXT,
data TEXT NOT NULL,
password_history TEXT,
deleted_at DATETIME
);
-- 5. Transfer content from X into new_X using a statement like:
-- INSERT INTO new_X SELECT ... FROM X.
INSERT INTO new_ciphers(uuid, created_at, updated_at, user_uuid, organization_uuid, atype,
name, notes, fields, data, password_history, deleted_at)
SELECT uuid, created_at, updated_at, user_uuid, organization_uuid, atype,
name, notes, fields, data, password_history, deleted_at
FROM ciphers;
-- 6. Drop the old table X: DROP TABLE X.
DROP TABLE ciphers;
-- 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X.
ALTER TABLE new_ciphers RENAME TO ciphers;
-- 11. Commit the transaction started in step 2.
-- 12. If foreign keys constraints were originally enabled, reenable them now.
--
-- `PRAGMA foreign_keys` is scoped to a database connection, and Diesel
-- migrations are run in a separate database connection that is closed once
-- the migrations finish.

@ -303,7 +303,6 @@ pub fn update_cipher_from_data(
type_data["PasswordHistory"] = data.PasswordHistory.clone().unwrap_or(Value::Null); type_data["PasswordHistory"] = data.PasswordHistory.clone().unwrap_or(Value::Null);
// TODO: ******* Backwards compat end ********** // TODO: ******* Backwards compat end **********
cipher.favorite = data.Favorite.unwrap_or(false);
cipher.name = data.Name; cipher.name = data.Name;
cipher.notes = data.Notes; cipher.notes = data.Notes;
cipher.fields = data.Fields.map(|f| f.to_string()); cipher.fields = data.Fields.map(|f| f.to_string());
@ -312,6 +311,7 @@ pub fn update_cipher_from_data(
cipher.save(&conn)?; cipher.save(&conn)?;
cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn)?; cipher.move_to_folder(data.FolderId, &headers.user.uuid, &conn)?;
cipher.set_favorite(data.Favorite, &headers.user.uuid, &conn)?;
if ut != UpdateType::None { if ut != UpdateType::None {
nt.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn)); nt.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
@ -410,6 +410,11 @@ fn put_cipher(uuid: String, data: JsonUpcase<CipherData>, headers: Headers, conn
None => err!("Cipher doesn't exist"), None => err!("Cipher doesn't exist"),
}; };
// TODO: Check if only the folder ID or favorite status is being changed.
// These are per-user properties that technically aren't part of the
// cipher itself, so the user shouldn't need write access to change these.
// Interestingly, upstream Bitwarden doesn't properly handle this either.
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) { if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
err!("Cipher is not write accessible") err!("Cipher is not write accessible")
} }

@ -32,7 +32,6 @@ pub struct Cipher {
pub data: String, pub data: String,
pub favorite: bool,
pub password_history: Option<String>, pub password_history: Option<String>,
pub deleted_at: Option<NaiveDateTime>, pub deleted_at: Option<NaiveDateTime>,
} }
@ -51,7 +50,6 @@ impl Cipher {
organization_uuid: None, organization_uuid: None,
atype, atype,
favorite: false,
name, name,
notes: None, notes: None,
@ -128,7 +126,7 @@ impl Cipher {
"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))), "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.is_favorite(&user_uuid, &conn),
"OrganizationId": self.organization_uuid, "OrganizationId": self.organization_uuid,
"Attachments": attachments_json, "Attachments": attachments_json,
"OrganizationUseTotp": true, "OrganizationUseTotp": true,
@ -337,6 +335,50 @@ impl Cipher {
self.get_access_restrictions(&user_uuid, &conn).is_some() self.get_access_restrictions(&user_uuid, &conn).is_some()
} }
// Returns whether this cipher is a favorite of the specified user.
pub fn is_favorite(&self, user_uuid: &str, conn: &DbConn) -> bool {
let query = favorites::table
.filter(favorites::user_uuid.eq(user_uuid))
.filter(favorites::cipher_uuid.eq(&self.uuid))
.count();
query.first::<i64>(&**conn).ok().unwrap_or(0) != 0
}
// Updates whether this cipher is a favorite of the specified user.
pub fn set_favorite(&self, favorite: Option<bool>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
if favorite.is_none() {
// No change requested.
return Ok(());
}
let (old, new) = (self.is_favorite(user_uuid, &conn), favorite.unwrap());
match (old, new) {
(false, true) => {
User::update_uuid_revision(user_uuid, &conn);
diesel::insert_into(favorites::table)
.values((
favorites::user_uuid.eq(user_uuid),
favorites::cipher_uuid.eq(&self.uuid),
))
.execute(&**conn)
.map_res("Error adding favorite")
}
(true, false) => {
User::update_uuid_revision(user_uuid, &conn);
diesel::delete(
favorites::table
.filter(favorites::user_uuid.eq(user_uuid))
.filter(favorites::cipher_uuid.eq(&self.uuid))
)
.execute(&**conn)
.map_res("Error removing favorite")
}
// Otherwise, the favorite status is already what it should be.
_ => Ok(())
}
}
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> { pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
folders_ciphers::table folders_ciphers::table
.inner_join(folders::table) .inner_join(folders::table)

@ -1,7 +1,7 @@
table! { table! {
attachments (id) { attachments (id) {
id -> Varchar, id -> Text,
cipher_uuid -> Varchar, cipher_uuid -> Text,
file_name -> Text, file_name -> Text,
file_size -> Integer, file_size -> Integer,
akey -> Nullable<Text>, akey -> Nullable<Text>,
@ -10,17 +10,16 @@ table! {
table! { table! {
ciphers (uuid) { ciphers (uuid) {
uuid -> Varchar, uuid -> Text,
created_at -> Datetime, created_at -> Datetime,
updated_at -> Datetime, updated_at -> Datetime,
user_uuid -> Nullable<Varchar>, user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Varchar>, organization_uuid -> Nullable<Text>,
atype -> Integer, atype -> Integer,
name -> Text, name -> Text,
notes -> Nullable<Text>, notes -> Nullable<Text>,
fields -> Nullable<Text>, fields -> Nullable<Text>,
data -> Text, data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>, password_history -> Nullable<Text>,
deleted_at -> Nullable<Datetime>, deleted_at -> Nullable<Datetime>,
} }
@ -28,25 +27,25 @@ table! {
table! { table! {
ciphers_collections (cipher_uuid, collection_uuid) { ciphers_collections (cipher_uuid, collection_uuid) {
cipher_uuid -> Varchar, cipher_uuid -> Text,
collection_uuid -> Varchar, collection_uuid -> Text,
} }
} }
table! { table! {
collections (uuid) { collections (uuid) {
uuid -> Varchar, uuid -> Text,
org_uuid -> Varchar, org_uuid -> Text,
name -> Text, name -> Text,
} }
} }
table! { table! {
devices (uuid) { devices (uuid) {
uuid -> Varchar, uuid -> Text,
created_at -> Datetime, created_at -> Datetime,
updated_at -> Datetime, updated_at -> Datetime,
user_uuid -> Varchar, user_uuid -> Text,
name -> Text, name -> Text,
atype -> Integer, atype -> Integer,
push_token -> Nullable<Text>, push_token -> Nullable<Text>,
@ -55,33 +54,40 @@ table! {
} }
} }
table! {
favorites (user_uuid, cipher_uuid) {
user_uuid -> Text,
cipher_uuid -> Text,
}
}
table! { table! {
folders (uuid) { folders (uuid) {
uuid -> Varchar, uuid -> Text,
created_at -> Datetime, created_at -> Datetime,
updated_at -> Datetime, updated_at -> Datetime,
user_uuid -> Varchar, user_uuid -> Text,
name -> Text, name -> Text,
} }
} }
table! { table! {
folders_ciphers (cipher_uuid, folder_uuid) { folders_ciphers (cipher_uuid, folder_uuid) {
cipher_uuid -> Varchar, cipher_uuid -> Text,
folder_uuid -> Varchar, folder_uuid -> Text,
} }
} }
table! { table! {
invitations (email) { invitations (email) {
email -> Varchar, email -> Text,
} }
} }
table! { table! {
org_policies (uuid) { org_policies (uuid) {
uuid -> Varchar, uuid -> Text,
org_uuid -> Varchar, org_uuid -> Text,
atype -> Integer, atype -> Integer,
enabled -> Bool, enabled -> Bool,
data -> Text, data -> Text,
@ -90,7 +96,7 @@ table! {
table! { table! {
organizations (uuid) { organizations (uuid) {
uuid -> Varchar, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
} }
@ -98,8 +104,8 @@ table! {
table! { table! {
twofactor (uuid) { twofactor (uuid) {
uuid -> Varchar, uuid -> Text,
user_uuid -> Varchar, user_uuid -> Text,
atype -> Integer, atype -> Integer,
enabled -> Bool, enabled -> Bool,
data -> Text, data -> Text,
@ -109,18 +115,18 @@ table! {
table! { table! {
users (uuid) { users (uuid) {
uuid -> Varchar, uuid -> Text,
created_at -> Datetime, created_at -> Datetime,
updated_at -> Datetime, updated_at -> Datetime,
verified_at -> Nullable<Datetime>, verified_at -> Nullable<Datetime>,
last_verifying_at -> Nullable<Datetime>, last_verifying_at -> Nullable<Datetime>,
login_verify_count -> Integer, login_verify_count -> Integer,
email -> Varchar, email -> Text,
email_new -> Nullable<Varchar>, email_new -> Nullable<Text>,
email_new_token -> Nullable<Varchar>, email_new_token -> Nullable<Text>,
name -> Text, name -> Text,
password_hash -> Blob, password_hash -> Binary,
salt -> Blob, salt -> Binary,
password_iterations -> Integer, password_iterations -> Integer,
password_hint -> Nullable<Text>, password_hint -> Nullable<Text>,
akey -> Text, akey -> Text,
@ -138,8 +144,8 @@ table! {
table! { table! {
users_collections (user_uuid, collection_uuid) { users_collections (user_uuid, collection_uuid) {
user_uuid -> Varchar, user_uuid -> Text,
collection_uuid -> Varchar, collection_uuid -> Text,
read_only -> Bool, read_only -> Bool,
hide_passwords -> Bool, hide_passwords -> Bool,
} }
@ -147,9 +153,9 @@ table! {
table! { table! {
users_organizations (uuid) { users_organizations (uuid) {
uuid -> Varchar, uuid -> Text,
user_uuid -> Varchar, user_uuid -> Text,
org_uuid -> Varchar, org_uuid -> Text,
access_all -> Bool, access_all -> Bool,
akey -> Text, akey -> Text,
status -> Integer, status -> Integer,

@ -20,7 +20,6 @@ table! {
notes -> Nullable<Text>, notes -> Nullable<Text>,
fields -> Nullable<Text>, fields -> Nullable<Text>,
data -> Text, data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>, password_history -> Nullable<Text>,
deleted_at -> Nullable<Timestamp>, deleted_at -> Nullable<Timestamp>,
} }
@ -55,6 +54,13 @@ table! {
} }
} }
table! {
favorites (user_uuid, cipher_uuid) {
user_uuid -> Text,
cipher_uuid -> Text,
}
}
table! { table! {
folders (uuid) { folders (uuid) {
uuid -> Text, uuid -> Text,

@ -20,7 +20,6 @@ table! {
notes -> Nullable<Text>, notes -> Nullable<Text>,
fields -> Nullable<Text>, fields -> Nullable<Text>,
data -> Text, data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>, password_history -> Nullable<Text>,
deleted_at -> Nullable<Timestamp>, deleted_at -> Nullable<Timestamp>,
} }
@ -55,6 +54,13 @@ table! {
} }
} }
table! {
favorites (user_uuid, cipher_uuid) {
user_uuid -> Text,
cipher_uuid -> Text,
}
}
table! { table! {
folders (uuid) { folders (uuid) {
uuid -> Text, uuid -> Text,

@ -313,12 +313,23 @@ mod migrations {
// Disable Foreign Key Checks during migration // Disable Foreign Key Checks during migration
use diesel::RunQueryDsl; use diesel::RunQueryDsl;
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
// "SET CONSTRAINTS sets the behavior of constraint checking within the
// current transaction", so this setting probably won't take effect for
// any of the migrations since it's being run outside of a transaction.
// Migrations that need to disable foreign key checks should run this
// from within the migration script itself.
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
diesel::sql_query("SET CONSTRAINTS ALL DEFERRED").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations"); diesel::sql_query("SET CONSTRAINTS ALL DEFERRED").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations");
// Scoped to a connection/session.
#[cfg(feature = "mysql")] #[cfg(feature = "mysql")]
diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations"); diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations");
// Scoped to a connection.
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations"); diesel::sql_query("PRAGMA foreign_keys = OFF").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations");
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations"); embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
} }

Loading…
Cancel
Save