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, );