mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-28 00:40:01 +02:00 
			
		
		
		
	Add support for hiding passwords in a collection
Ref: https://github.com/bitwarden/server/pull/743
This commit is contained in:
		| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE users_collections | ||||||
|  | ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE; | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE users_collections | ||||||
|  | ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE; | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE users_collections | ||||||
|  | ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT 0; -- FALSE | ||||||
| @@ -374,7 +374,7 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders, | |||||||
|         .map(|col_user| { |         .map(|col_user| { | ||||||
|             UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn) |             UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn) | ||||||
|                 .unwrap() |                 .unwrap() | ||||||
|                 .to_json_read_only(col_user.read_only) |                 .to_json_user_access_restrictions(&col_user) | ||||||
|         }) |         }) | ||||||
|         .collect(); |         .collect(); | ||||||
|  |  | ||||||
| @@ -408,7 +408,9 @@ fn put_collection_users( | |||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         CollectionUser::save(&user.user_uuid, &coll_id, d.ReadOnly, &conn)?; |         CollectionUser::save(&user.user_uuid, &coll_id, | ||||||
|  |                              d.ReadOnly, d.HidePasswords, | ||||||
|  |                              &conn)?; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
| @@ -452,6 +454,7 @@ fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonRe | |||||||
| struct CollectionData { | struct CollectionData { | ||||||
|     Id: String, |     Id: String, | ||||||
|     ReadOnly: bool, |     ReadOnly: bool, | ||||||
|  |     HidePasswords: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| @@ -523,7 +526,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade | |||||||
|                 match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { |                 match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { | ||||||
|                     None => err!("Collection not found in Organization"), |                     None => err!("Collection not found in Organization"), | ||||||
|                     Some(collection) => { |                     Some(collection) => { | ||||||
|                         CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn)?; |                         CollectionUser::save(&user.uuid, &collection.uuid, | ||||||
|  |                                              col.ReadOnly, col.HidePasswords, | ||||||
|  |                                              &conn)?; | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -778,7 +783,9 @@ fn edit_user( | |||||||
|             match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { |             match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) { | ||||||
|                 None => err!("Collection not found in Organization"), |                 None => err!("Collection not found in Organization"), | ||||||
|                 Some(collection) => { |                 Some(collection) => { | ||||||
|                     CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, &conn)?; |                     CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, | ||||||
|  |                                          col.ReadOnly, col.HidePasswords, | ||||||
|  |                                          &conn)?; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -82,6 +82,15 @@ impl Cipher { | |||||||
|         let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); |         let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); | ||||||
|         let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); |         let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); | ||||||
|  |  | ||||||
|  |         let (read_only, hide_passwords) = | ||||||
|  |             match self.get_access_restrictions(&user_uuid, &conn) { | ||||||
|  |                 Some((ro, hp)) => (ro, hp), | ||||||
|  |                 None => { | ||||||
|  |                     error!("Cipher ownership assertion failure"); | ||||||
|  |                     (true, true) | ||||||
|  |                 }, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|         // Get the data or a default empty value to avoid issues with the mobile apps |         // Get the data or a default empty value to avoid issues with the mobile apps | ||||||
|         let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({ |         let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({ | ||||||
|             "Fields":null, |             "Fields":null, | ||||||
| @@ -105,7 +114,15 @@ impl Cipher { | |||||||
|         } |         } | ||||||
|         // TODO: ******* Backwards compat end ********** |         // TODO: ******* Backwards compat end ********** | ||||||
|  |  | ||||||
|  |         // There are three types of cipher response models in upstream | ||||||
|  |         // Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order | ||||||
|  |         // of increasing level of detail). bitwarden_rs currently only | ||||||
|  |         // supports the "cipherDetails" type, though it seems like the | ||||||
|  |         // Bitwarden clients will ignore extra fields. | ||||||
|  |         // | ||||||
|  |         // Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs | ||||||
|         let mut json_object = json!({ |         let mut json_object = json!({ | ||||||
|  |             "Object": "cipherDetails", | ||||||
|             "Id": self.uuid, |             "Id": self.uuid, | ||||||
|             "Type": self.atype, |             "Type": self.atype, | ||||||
|             "RevisionDate": format_date(&self.updated_at), |             "RevisionDate": format_date(&self.updated_at), | ||||||
| @@ -115,6 +132,8 @@ impl Cipher { | |||||||
|             "OrganizationId": self.organization_uuid, |             "OrganizationId": self.organization_uuid, | ||||||
|             "Attachments": attachments_json, |             "Attachments": attachments_json, | ||||||
|             "OrganizationUseTotp": true, |             "OrganizationUseTotp": true, | ||||||
|  |  | ||||||
|  |             // This field is specific to the cipherDetails type. | ||||||
|             "CollectionIds": self.get_collections(user_uuid, &conn), |             "CollectionIds": self.get_collections(user_uuid, &conn), | ||||||
|  |  | ||||||
|             "Name": self.name, |             "Name": self.name, | ||||||
| @@ -123,8 +142,11 @@ impl Cipher { | |||||||
|  |  | ||||||
|             "Data": data_json, |             "Data": data_json, | ||||||
|  |  | ||||||
|             "Object": "cipher", |             // These values are true by default, but can be false if the | ||||||
|             "Edit": true, |             // cipher belongs to a collection where the org owner has enabled | ||||||
|  |             // the "Read Only" or "Hide Passwords" restrictions for the user. | ||||||
|  |             "Edit": !read_only, | ||||||
|  |             "ViewPassword": !hide_passwords, | ||||||
|  |  | ||||||
|             "PasswordHistory": password_history_json, |             "PasswordHistory": password_history_json, | ||||||
|         }); |         }); | ||||||
| @@ -241,66 +263,97 @@ impl Cipher { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { |     /// Returns whether this cipher is directly owned by the user. | ||||||
|  |     pub fn is_owned_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool { | ||||||
|         ciphers::table |         ciphers::table | ||||||
|             .filter(ciphers::uuid.eq(&self.uuid)) |             .filter(ciphers::uuid.eq(&self.uuid)) | ||||||
|             .left_join( |             .filter(ciphers::user_uuid.eq(&user_uuid)) | ||||||
|                 users_organizations::table.on(ciphers::organization_uuid |  | ||||||
|                     .eq(users_organizations::org_uuid.nullable()) |  | ||||||
|                     .and(users_organizations::user_uuid.eq(user_uuid))), |  | ||||||
|             ) |  | ||||||
|             .left_join(ciphers_collections::table) |  | ||||||
|             .left_join( |  | ||||||
|                 users_collections::table |  | ||||||
|                     .on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)), |  | ||||||
|             ) |  | ||||||
|             .filter(ciphers::user_uuid.eq(user_uuid).or( |  | ||||||
|                 // Cipher owner |  | ||||||
|                 users_organizations::access_all.eq(true).or( |  | ||||||
|                     // access_all in Organization |  | ||||||
|                     users_organizations::atype.le(UserOrgType::Admin as i32).or( |  | ||||||
|                         // Org admin or owner |  | ||||||
|                         users_collections::user_uuid.eq(user_uuid).and( |  | ||||||
|                             users_collections::read_only.eq(false), //R/W access to collection |  | ||||||
|                         ), |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             )) |  | ||||||
|             .select(ciphers::all_columns) |  | ||||||
|             .first::<Self>(&**conn) |             .first::<Self>(&**conn) | ||||||
|             .ok() |             .ok() | ||||||
|             .is_some() |             .is_some() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { |     /// Returns whether this cipher is owned by an org in which the user has full access. | ||||||
|  |     pub fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool { | ||||||
|         ciphers::table |         ciphers::table | ||||||
|             .filter(ciphers::uuid.eq(&self.uuid)) |             .filter(ciphers::uuid.eq(&self.uuid)) | ||||||
|             .left_join( |             .inner_join(ciphers_collections::table.on( | ||||||
|                 users_organizations::table.on(ciphers::organization_uuid |                 ciphers::uuid.eq(ciphers_collections::cipher_uuid))) | ||||||
|                     .eq(users_organizations::org_uuid.nullable()) |             .inner_join(users_organizations::table.on( | ||||||
|                     .and(users_organizations::user_uuid.eq(user_uuid))), |                 ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) | ||||||
|             ) |                     .and(users_organizations::user_uuid.eq(user_uuid)) | ||||||
|             .left_join(ciphers_collections::table) |                     .and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)))) | ||||||
|             .left_join( |             // The user is an org admin or higher. | ||||||
|                 users_collections::table |             .filter(users_organizations::atype.le(UserOrgType::Admin as i32)) | ||||||
|                     .on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)), |             // The user was granted full access to the org by an org owner/admin. | ||||||
|             ) |             .or_filter(users_organizations::access_all.eq(true)) | ||||||
|             .filter(ciphers::user_uuid.eq(user_uuid).or( |             .select(ciphers::uuid) | ||||||
|                 // Cipher owner |             .first::<String>(&**conn) | ||||||
|                 users_organizations::access_all.eq(true).or( |  | ||||||
|                     // access_all in Organization |  | ||||||
|                     users_organizations::atype.le(UserOrgType::Admin as i32).or( |  | ||||||
|                         // Org admin or owner |  | ||||||
|                         users_collections::user_uuid.eq(user_uuid), // Access to Collection |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             )) |  | ||||||
|             .select(ciphers::all_columns) |  | ||||||
|             .first::<Self>(&**conn) |  | ||||||
|             .ok() |             .ok() | ||||||
|             .is_some() |             .is_some() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Returns the user's access restrictions to this cipher. A return value | ||||||
|  |     /// of None means that this cipher does not belong to the user, and is | ||||||
|  |     /// not in any collection the user has access to. Otherwise, the user has | ||||||
|  |     /// access to this cipher, and Some(read_only, hide_passwords) represents | ||||||
|  |     /// the access restrictions. | ||||||
|  |     pub fn get_access_restrictions(&self, user_uuid: &str, conn: &DbConn) -> Option<(bool, bool)> { | ||||||
|  |         // Check whether this cipher is directly owned by the user, or is in | ||||||
|  |         // a collection that the user has full access to. If so, there are no | ||||||
|  |         // access restrictions. | ||||||
|  |         if self.is_owned_by_user(&user_uuid, &conn) || self.is_in_full_access_org(&user_uuid, &conn) { | ||||||
|  |             return Some((false, false)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check whether this cipher is in any collections accessible to the | ||||||
|  |         // user. If so, retrieve the access flags for each collection. | ||||||
|  |         let query = ciphers::table | ||||||
|  |             .filter(ciphers::uuid.eq(&self.uuid)) | ||||||
|  |             .inner_join(ciphers_collections::table.on( | ||||||
|  |                 ciphers::uuid.eq(ciphers_collections::cipher_uuid))) | ||||||
|  |             .inner_join(users_collections::table.on( | ||||||
|  |                 ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) | ||||||
|  |                     .and(users_collections::user_uuid.eq(user_uuid)))) | ||||||
|  |             .select((users_collections::read_only, users_collections::hide_passwords)); | ||||||
|  |  | ||||||
|  |         // There's an edge case where a cipher can be in multiple collections | ||||||
|  |         // with inconsistent access flags. For example, a cipher could be in | ||||||
|  |         // one collection where the user has read-only access, but also in | ||||||
|  |         // another collection where the user has read/write access. To handle | ||||||
|  |         // this, we do a boolean OR of all values in each of the `read_only` | ||||||
|  |         // and `hide_passwords` columns. This could ideally be done as part | ||||||
|  |         // of the query, but Diesel doesn't support a max() or bool_or() | ||||||
|  |         // function on booleans and this behavior isn't portable anyway. | ||||||
|  |         match query.load::<(bool, bool)>(&**conn).ok() { | ||||||
|  |             Some(vec) => { | ||||||
|  |                 let mut read_only = false; | ||||||
|  |                 let mut hide_passwords = false; | ||||||
|  |                 for (ro, hp) in vec.iter() { | ||||||
|  |                     read_only |= ro; | ||||||
|  |                     hide_passwords |= hp; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 Some((read_only, hide_passwords)) | ||||||
|  |             }, | ||||||
|  |             None => { | ||||||
|  |                 // This cipher isn't in any collections accessible to the user. | ||||||
|  |                 None | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { | ||||||
|  |         match self.get_access_restrictions(&user_uuid, &conn) { | ||||||
|  |             Some((read_only, _hide_passwords)) => !read_only, | ||||||
|  |             None => false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { | ||||||
|  |         self.get_access_restrictions(&user_uuid, &conn).is_some() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     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) | ||||||
|   | |||||||
| @@ -199,6 +199,7 @@ pub struct CollectionUser { | |||||||
|     pub user_uuid: String, |     pub user_uuid: String, | ||||||
|     pub collection_uuid: String, |     pub collection_uuid: String, | ||||||
|     pub read_only: bool, |     pub read_only: bool, | ||||||
|  |     pub hide_passwords: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Database methods | /// Database methods | ||||||
| @@ -214,7 +215,7 @@ impl CollectionUser { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[cfg(feature = "postgresql")] |     #[cfg(feature = "postgresql")] | ||||||
|     pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult { |     pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult { | ||||||
|         User::update_uuid_revision(&user_uuid, conn); |         User::update_uuid_revision(&user_uuid, conn); | ||||||
|  |  | ||||||
|         diesel::insert_into(users_collections::table) |         diesel::insert_into(users_collections::table) | ||||||
| @@ -222,16 +223,18 @@ impl CollectionUser { | |||||||
|                 users_collections::user_uuid.eq(user_uuid), |                 users_collections::user_uuid.eq(user_uuid), | ||||||
|                 users_collections::collection_uuid.eq(collection_uuid), |                 users_collections::collection_uuid.eq(collection_uuid), | ||||||
|                 users_collections::read_only.eq(read_only), |                 users_collections::read_only.eq(read_only), | ||||||
|  |                 users_collections::hide_passwords.eq(hide_passwords), | ||||||
|             )) |             )) | ||||||
|             .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) |             .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) | ||||||
|             .do_update() |             .do_update() | ||||||
|             .set(users_collections::read_only.eq(read_only)) |             .set(users_collections::read_only.eq(read_only)) | ||||||
|  |             .set(users_collections::hide_passwords.eq(hide_passwords)) | ||||||
|             .execute(&**conn) |             .execute(&**conn) | ||||||
|             .map_res("Error adding user to collection") |             .map_res("Error adding user to collection") | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[cfg(not(feature = "postgresql"))] |     #[cfg(not(feature = "postgresql"))] | ||||||
|     pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult { |     pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult { | ||||||
|         User::update_uuid_revision(&user_uuid, conn); |         User::update_uuid_revision(&user_uuid, conn); | ||||||
|  |  | ||||||
|         diesel::replace_into(users_collections::table) |         diesel::replace_into(users_collections::table) | ||||||
| @@ -239,6 +242,7 @@ impl CollectionUser { | |||||||
|                 users_collections::user_uuid.eq(user_uuid), |                 users_collections::user_uuid.eq(user_uuid), | ||||||
|                 users_collections::collection_uuid.eq(collection_uuid), |                 users_collections::collection_uuid.eq(collection_uuid), | ||||||
|                 users_collections::read_only.eq(read_only), |                 users_collections::read_only.eq(read_only), | ||||||
|  |                 users_collections::hide_passwords.eq(hide_passwords), | ||||||
|             )) |             )) | ||||||
|             .execute(&**conn) |             .execute(&**conn) | ||||||
|             .map_res("Error adding user to collection") |             .map_res("Error adding user to collection") | ||||||
|   | |||||||
| @@ -310,10 +310,11 @@ impl UserOrganization { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn to_json_read_only(&self, read_only: bool) -> Value { |     pub fn to_json_user_access_restrictions(&self, col_user: &CollectionUser) -> Value { | ||||||
|         json!({ |         json!({ | ||||||
|             "Id": self.uuid, |             "Id": self.uuid, | ||||||
|             "ReadOnly": read_only |             "ReadOnly": col_user.read_only, | ||||||
|  |             "HidePasswords": col_user.hide_passwords, | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -324,7 +325,11 @@ impl UserOrganization { | |||||||
|             let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn); |             let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn); | ||||||
|             collections |             collections | ||||||
|                 .iter() |                 .iter() | ||||||
|                 .map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only})) |                 .map(|c| json!({ | ||||||
|  |                     "Id": c.collection_uuid, | ||||||
|  |                     "ReadOnly": c.read_only, | ||||||
|  |                     "HidePasswords": c.hide_passwords, | ||||||
|  |                 })) | ||||||
|                 .collect() |                 .collect() | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -141,6 +141,7 @@ table! { | |||||||
|         user_uuid -> Varchar, |         user_uuid -> Varchar, | ||||||
|         collection_uuid -> Varchar, |         collection_uuid -> Varchar, | ||||||
|         read_only -> Bool, |         read_only -> Bool, | ||||||
|  |         hide_passwords -> Bool, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -141,6 +141,7 @@ table! { | |||||||
|         user_uuid -> Text, |         user_uuid -> Text, | ||||||
|         collection_uuid -> Text, |         collection_uuid -> Text, | ||||||
|         read_only -> Bool, |         read_only -> Bool, | ||||||
|  |         hide_passwords -> Bool, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -141,6 +141,7 @@ table! { | |||||||
|         user_uuid -> Text, |         user_uuid -> Text, | ||||||
|         collection_uuid -> Text, |         collection_uuid -> Text, | ||||||
|         read_only -> Bool, |         read_only -> Bool, | ||||||
|  |         hide_passwords -> Bool, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user