From 34f2aa68f431773d040124db0be9d4fb8eb03367 Mon Sep 17 00:00:00 2001 From: Miroslav Prasil Date: Wed, 9 May 2018 11:55:05 +0100 Subject: [PATCH 1/4] Implement Collection-Cipher mapping --- .../down.sql | 1 + .../up.sql | 5 ++ src/api/core/ciphers.rs | 41 +++++++++++++ src/api/core/mod.rs | 1 + src/db/models/cipher.rs | 9 ++- src/db/models/collection.rs | 59 ++++++++++++++++++- src/db/models/mod.rs | 2 +- src/db/schema.rs | 10 ++++ 8 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 migrations/2018-05-08-161616_create_collection_cipher_map/down.sql create mode 100644 migrations/2018-05-08-161616_create_collection_cipher_map/up.sql diff --git a/migrations/2018-05-08-161616_create_collection_cipher_map/down.sql b/migrations/2018-05-08-161616_create_collection_cipher_map/down.sql new file mode 100644 index 0000000..ba973f4 --- /dev/null +++ b/migrations/2018-05-08-161616_create_collection_cipher_map/down.sql @@ -0,0 +1 @@ +DROP TABLE ciphers_collections; \ No newline at end of file diff --git a/migrations/2018-05-08-161616_create_collection_cipher_map/up.sql b/migrations/2018-05-08-161616_create_collection_cipher_map/up.sql new file mode 100644 index 0000000..9fdd706 --- /dev/null +++ b/migrations/2018-05-08-161616_create_collection_cipher_map/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE ciphers_collections ( + cipher_uuid TEXT NOT NULL REFERENCES ciphers (uuid), + collection_uuid TEXT NOT NULL REFERENCES collections (uuid), + PRIMARY KEY (cipher_uuid, collection_uuid) +); \ No newline at end of file diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 8b22b57..afdf7e7 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::collections::HashSet; use rocket::Data; use rocket::http::ContentType; @@ -297,6 +298,46 @@ fn put_cipher(uuid: String, data: Json, headers: Headers, conn: DbCo Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) } +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct CollectionsAdminData { + collectionIds: Vec, +} + +#[post("/ciphers//collections-admin", data = "")] +fn post_collections_admin(uuid: String, data: Json, headers: Headers, conn: DbConn) -> EmptyResult { + let data: CollectionsAdminData = data.into_inner(); + + let 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 is not write accessible") + } + + let posted_collections: HashSet = data.collectionIds.iter().cloned().collect(); + let current_collections: HashSet = cipher.get_collections(&conn).iter().cloned().collect(); + + //TODO: update cipher collection mapping + for collection in posted_collections.symmetric_difference(¤t_collections) { + match Collection::find_by_uuid(&collection, &conn) { + None => (), // Does not exist, what now? + Some(collection) => { + if collection.is_writable_by_user(&headers.user.uuid, &conn) { + if posted_collections.contains(&collection.uuid) { // Add to collection + CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn); + } else { // Remove from collection + CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn); + } + } + } + } + } + + Ok(()) +} #[post("/ciphers//attachment", format = "multipart/form-data", data = "")] fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn) -> JsonResult { diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index f9c8839..1f6daf5 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -67,6 +67,7 @@ pub fn routes() -> Vec { post_organization, post_organization_collections, post_organization_collection_update, + post_collections_admin, get_org_details, get_org_users, send_invite, diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 3d3749f..94077e5 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -98,7 +98,7 @@ impl Cipher { "OrganizationId": self.organization_uuid, "Attachments": attachments_json, "OrganizationUseTotp": false, - "CollectionIds": [], + "CollectionIds": self.get_collections(&conn), "Name": self.name, "Notes": self.notes, @@ -241,4 +241,11 @@ impl Cipher { .select(ciphers::all_columns) .load::(&**conn).expect("Error loading ciphers") } + + pub fn get_collections(&self, conn: &DbConn) -> Vec { + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .select(ciphers_collections::collection_uuid) + .load::(&**conn).unwrap_or(vec![]) + } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 425cf8d..265085f 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -2,7 +2,7 @@ use serde_json::Value as JsonValue; use uuid::Uuid; -use super::Organization; +use super::{Organization, UserOrganization}; #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] #[table_name = "collections"] @@ -100,6 +100,27 @@ impl Collection { .select(collections::all_columns) .first::(&**conn).ok() } + + pub fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool { + match UserOrganization::find_by_user_and_org(&user_uuid, &self.org_uuid, &conn) { + None => false, // Not in Org + Some(user_org) => { + if user_org.access_all { + true + } else { + match users_collections::table.inner_join(collections::table) + .filter(users_collections::collection_uuid.eq(&self.uuid)) + .filter(users_collections::user_uuid.eq(&user_uuid)) + .filter(users_collections::read_only.eq(false)) + .select(collections::all_columns) + .first::(&**conn).ok() { + None => false, // Read only or no access to collection + Some(_) => true, + } + } + } + } + } } use super::User; @@ -147,4 +168,40 @@ impl CollectionUsers { _ => false, } } +} + +use super::Cipher; + +#[derive(Debug, Identifiable, Queryable, Insertable, Associations)] +#[table_name = "ciphers_collections"] +#[belongs_to(Cipher, foreign_key = "cipher_uuid")] +#[belongs_to(Collection, foreign_key = "collection_uuid")] +#[primary_key(cipher_uuid, collection_uuid)] +pub struct CollectionCipher { + pub cipher_uuid: String, + pub collection_uuid: String, +} + +/// Database methods +impl CollectionCipher { + pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool { + match diesel::replace_into(ciphers_collections::table) + .values(( + ciphers_collections::cipher_uuid.eq(cipher_uuid), + ciphers_collections::collection_uuid.eq(collection_uuid), + )).execute(&**conn) { + Ok(1) => true, // One row inserted + _ => false, + } + } + + pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool { + match diesel::delete(ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)) + .filter(ciphers_collections::collection_uuid.eq(collection_uuid))) + .execute(&**conn) { + Ok(1) => true, // One row deleted + _ => false, + } + } } \ No newline at end of file diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index eb7f02c..70d1548 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -14,4 +14,4 @@ pub use self::folder::{Folder, FolderCipher}; pub use self::user::User; pub use self::organization::Organization; pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType}; -pub use self::collection::{Collection, CollectionUsers}; +pub use self::collection::{Collection, CollectionUsers, CollectionCipher}; diff --git a/src/db/schema.rs b/src/db/schema.rs index 7633665..c144e9b 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -101,6 +101,13 @@ table! { } } +table! { + ciphers_collections (cipher_uuid, collection_uuid) { + cipher_uuid -> Text, + collection_uuid -> Text, + } +} + table! { users_organizations (uuid) { uuid -> Text, @@ -124,6 +131,8 @@ joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); +joinable!(ciphers_collections -> collections (collection_uuid)); +joinable!(ciphers_collections -> ciphers (cipher_uuid)); joinable!(users_organizations -> organizations (org_uuid)); joinable!(users_organizations -> users (user_uuid)); @@ -137,5 +146,6 @@ allow_tables_to_appear_in_same_query!( organizations, users, users_collections, + ciphers_collections, users_organizations, ); From e5c9d19e2596af85d37222c93a9b6a812ad25b4e Mon Sep 17 00:00:00 2001 From: Miroslav Prasil Date: Fri, 11 May 2018 11:32:50 +0100 Subject: [PATCH 2/4] Remove outdated comment --- src/api/core/ciphers.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index afdf7e7..0afe760 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -320,7 +320,6 @@ fn post_collections_admin(uuid: String, data: Json, header let posted_collections: HashSet = data.collectionIds.iter().cloned().collect(); let current_collections: HashSet = cipher.get_collections(&conn).iter().cloned().collect(); - //TODO: update cipher collection mapping for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid(&collection, &conn) { None => (), // Does not exist, what now? From 9cf449e1c529ab7b21f25729050b35ca39cbc4e9 Mon Sep 17 00:00:00 2001 From: Miroslav Prasil Date: Fri, 11 May 2018 11:45:55 +0100 Subject: [PATCH 3/4] Error on invalid collection ID in post_collections_admin --- src/api/core/ciphers.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 0afe760..d3d4717 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -322,7 +322,7 @@ fn post_collections_admin(uuid: String, data: Json, header for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid(&collection, &conn) { - None => (), // Does not exist, what now? + None => err!("Invalid collection ID provided"), Some(collection) => { if collection.is_writable_by_user(&headers.user.uuid, &conn) { if posted_collections.contains(&collection.uuid) { // Add to collection @@ -330,6 +330,8 @@ fn post_collections_admin(uuid: String, data: Json, header } else { // Remove from collection CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn); } + } else { + err!("No rights to modify the collection") } } } From dfb12320817a58484d259011e49d9552b450f2bc Mon Sep 17 00:00:00 2001 From: Miroslav Prasil Date: Fri, 11 May 2018 14:24:41 +0100 Subject: [PATCH 4/4] Filter collection lists based on user --- src/api/core/ciphers.rs | 2 +- src/db/models/cipher.rs | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index d3d4717..a8ba1c5 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -318,7 +318,7 @@ fn post_collections_admin(uuid: String, data: Json, header } let posted_collections: HashSet = data.collectionIds.iter().cloned().collect(); - let current_collections: HashSet = cipher.get_collections(&conn).iter().cloned().collect(); + let current_collections: HashSet = cipher.get_collections(&headers.user.uuid ,&conn).iter().cloned().collect(); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid(&collection, &conn) { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 94077e5..e72c2ab 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -3,7 +3,7 @@ use serde_json::Value as JsonValue; use uuid::Uuid; -use super::{User, Organization, UserOrganization, FolderCipher}; +use super::{User, Organization, UserOrganization, FolderCipher, UserOrgType}; #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] #[table_name = "ciphers"] @@ -98,7 +98,7 @@ impl Cipher { "OrganizationId": self.organization_uuid, "Attachments": attachments_json, "OrganizationUseTotp": false, - "CollectionIds": self.get_collections(&conn), + "CollectionIds": self.get_collections(user_uuid, &conn), "Name": self.name, "Notes": self.notes, @@ -242,9 +242,25 @@ impl Cipher { .load::(&**conn).expect("Error loading ciphers") } - pub fn get_collections(&self, conn: &DbConn) -> Vec { + pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec { ciphers_collections::table + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid).and( + users_organizations::user_uuid.eq(user_id) + ) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + )) .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection + users_organizations::access_all.eq(true).or( // User has access all + users_organizations::type_.le(UserOrgType::Admin as i32) // User is admin or owner + ) + )) .select(ciphers_collections::collection_uuid) .load::(&**conn).unwrap_or(vec![]) }