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 ba8a1c27f7f88c4670b488fb0cc87d1575538ead
parent 032134aabcc78bca6f4b8beaf094c42a4b32a90c
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Fri, 11 May 2018 22:55:05 +0200

Merge pull request #13 from mprasil/cipher_collection

Implement Collection-Cipher mapping
Diffstat:
Amigrations/2018-05-08-161616_create_collection_cipher_map/down.sql | 2++
Amigrations/2018-05-08-161616_create_collection_cipher_map/up.sql | 6++++++
Msrc/api/core/ciphers.rs | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/api/core/mod.rs | 1+
Msrc/db/models/cipher.rs | 27+++++++++++++++++++++++++--
Msrc/db/models/collection.rs | 59++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/db/models/mod.rs | 2+-
Msrc/db/schema.rs | 10++++++++++
8 files changed, 145 insertions(+), 4 deletions(-)

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 @@ -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 @@ -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 @@ -1,4 +1,5 @@ use std::path::Path; +use std::collections::HashSet; use rocket::Data; use rocket::http::ContentType; @@ -297,6 +298,47 @@ fn put_cipher(uuid: String, data: Json<CipherData>, 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<String>, +} + +#[post("/ciphers/<uuid>/collections-admin", data = "<data>")] +fn post_collections_admin(uuid: String, data: Json<CollectionsAdminData>, 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<String> = data.collectionIds.iter().cloned().collect(); + let current_collections: HashSet<String> = cipher.get_collections(&headers.user.uuid ,&conn).iter().cloned().collect(); + + for collection in posted_collections.symmetric_difference(&current_collections) { + match Collection::find_by_uuid(&collection, &conn) { + 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 + CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn); + } else { // Remove from collection + CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn); + } + } else { + err!("No rights to modify the collection") + } + } + } + } + + Ok(()) +} #[post("/ciphers/<uuid>/attachment", format = "multipart/form-data", 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 @@ -67,6 +67,7 @@ pub fn routes() -> Vec<Route> { 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 @@ -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": [], + "CollectionIds": self.get_collections(user_uuid, &conn), "Name": self.name, "Notes": self.notes, @@ -241,4 +241,27 @@ impl Cipher { .select(ciphers::all_columns) .load::<Self>(&**conn).expect("Error loading ciphers") } + + pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec<String> { + 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::<String>(&**conn).unwrap_or(vec![]) + } } diff --git 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::<Self>(&**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::<Self>(&**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 @@ -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 @@ -102,6 +102,13 @@ table! { } table! { + ciphers_collections (cipher_uuid, collection_uuid) { + cipher_uuid -> Text, + collection_uuid -> Text, + } +} + +table! { users_organizations (uuid) { uuid -> Text, user_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, );