mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 18:28:20 +02:00 
			
		
		
		
	add bulk-access endpoint for collections (#5542)
This commit is contained in:
		| @@ -38,6 +38,7 @@ pub fn routes() -> Vec<Route> { | |||||||
|         post_organization_collections, |         post_organization_collections, | ||||||
|         delete_organization_collection_member, |         delete_organization_collection_member, | ||||||
|         post_organization_collection_delete_member, |         post_organization_collection_delete_member, | ||||||
|  |         post_bulk_access_collections, | ||||||
|         post_organization_collection_update, |         post_organization_collection_update, | ||||||
|         put_organization_collection_update, |         put_organization_collection_update, | ||||||
|         delete_organization_collection, |         delete_organization_collection, | ||||||
| @@ -129,17 +130,17 @@ struct OrganizationUpdateData { | |||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct NewCollectionData { | struct FullCollectionData { | ||||||
|     name: String, |     name: String, | ||||||
|     groups: Vec<NewCollectionGroupData>, |     groups: Vec<CollectionGroupData>, | ||||||
|     users: Vec<NewCollectionMemberData>, |     users: Vec<CollectionMembershipData>, | ||||||
|     id: Option<CollectionId>, |     id: Option<CollectionId>, | ||||||
|     external_id: Option<String>, |     external_id: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct NewCollectionGroupData { | struct CollectionGroupData { | ||||||
|     hide_passwords: bool, |     hide_passwords: bool, | ||||||
|     id: GroupId, |     id: GroupId, | ||||||
|     read_only: bool, |     read_only: bool, | ||||||
| @@ -148,7 +149,7 @@ struct NewCollectionGroupData { | |||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct NewCollectionMemberData { | struct CollectionMembershipData { | ||||||
|     hide_passwords: bool, |     hide_passwords: bool, | ||||||
|     id: MembershipId, |     id: MembershipId, | ||||||
|     read_only: bool, |     read_only: bool, | ||||||
| @@ -429,13 +430,13 @@ async fn _get_org_collections(org_id: &OrganizationId, conn: &mut DbConn) -> Val | |||||||
| async fn post_organization_collections( | async fn post_organization_collections( | ||||||
|     org_id: OrganizationId, |     org_id: OrganizationId, | ||||||
|     headers: ManagerHeadersLoose, |     headers: ManagerHeadersLoose, | ||||||
|     data: Json<NewCollectionData>, |     data: Json<FullCollectionData>, | ||||||
|     mut conn: DbConn, |     mut conn: DbConn, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     if org_id != headers.membership.org_uuid { |     if org_id != headers.membership.org_uuid { | ||||||
|         err!("Organization not found", "Organization id's do not match"); |         err!("Organization not found", "Organization id's do not match"); | ||||||
|     } |     } | ||||||
|     let data: NewCollectionData = data.into_inner(); |     let data: FullCollectionData = data.into_inner(); | ||||||
|  |  | ||||||
|     let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { |     let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { | ||||||
|         err!("Can't find organization details") |         err!("Can't find organization details") | ||||||
| @@ -488,29 +489,104 @@ async fn post_organization_collections( | |||||||
|     Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &mut conn).await)) |     Ok(Json(collection.to_json_details(&headers.membership.user_uuid, None, &mut conn).await)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize)] | ||||||
|  | #[serde(rename_all = "camelCase")] | ||||||
|  | struct BulkCollectionAccessData { | ||||||
|  |     collection_ids: Vec<CollectionId>, | ||||||
|  |     groups: Vec<CollectionGroupData>, | ||||||
|  |     users: Vec<CollectionMembershipData>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[post("/organizations/<org_id>/collections/bulk-access", data = "<data>", rank = 1)] | ||||||
|  | async fn post_bulk_access_collections( | ||||||
|  |     org_id: OrganizationId, | ||||||
|  |     headers: ManagerHeadersLoose, | ||||||
|  |     data: Json<BulkCollectionAccessData>, | ||||||
|  |     mut conn: DbConn, | ||||||
|  | ) -> EmptyResult { | ||||||
|  |     if org_id != headers.membership.org_uuid { | ||||||
|  |         err!("Organization not found", "Organization id's do not match"); | ||||||
|  |     } | ||||||
|  |     let data: BulkCollectionAccessData = data.into_inner(); | ||||||
|  |  | ||||||
|  |     if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() { | ||||||
|  |         err!("Can't find organization details") | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     for col_id in data.collection_ids { | ||||||
|  |         let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { | ||||||
|  |             err!("Collection not found") | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         // update collection modification date | ||||||
|  |         collection.save(&mut conn).await?; | ||||||
|  |  | ||||||
|  |         log_event( | ||||||
|  |             EventType::CollectionUpdated as i32, | ||||||
|  |             &collection.uuid, | ||||||
|  |             &org_id, | ||||||
|  |             &headers.user.uuid, | ||||||
|  |             headers.device.atype, | ||||||
|  |             &headers.ip.ip, | ||||||
|  |             &mut conn, | ||||||
|  |         ) | ||||||
|  |         .await; | ||||||
|  |  | ||||||
|  |         CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?; | ||||||
|  |         for group in &data.groups { | ||||||
|  |             CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage) | ||||||
|  |                 .save(&mut conn) | ||||||
|  |                 .await?; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?; | ||||||
|  |         for user in &data.users { | ||||||
|  |             let Some(member) = Membership::find_by_uuid_and_org(&user.id, &org_id, &mut conn).await else { | ||||||
|  |                 err!("User is not part of organization") | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if member.access_all { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             CollectionUser::save( | ||||||
|  |                 &member.user_uuid, | ||||||
|  |                 &col_id, | ||||||
|  |                 user.read_only, | ||||||
|  |                 user.hide_passwords, | ||||||
|  |                 user.manage, | ||||||
|  |                 &mut conn, | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
| #[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")] | #[put("/organizations/<org_id>/collections/<col_id>", data = "<data>")] | ||||||
| async fn put_organization_collection_update( | async fn put_organization_collection_update( | ||||||
|     org_id: OrganizationId, |     org_id: OrganizationId, | ||||||
|     col_id: CollectionId, |     col_id: CollectionId, | ||||||
|     headers: ManagerHeaders, |     headers: ManagerHeaders, | ||||||
|     data: Json<NewCollectionData>, |     data: Json<FullCollectionData>, | ||||||
|     conn: DbConn, |     conn: DbConn, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     post_organization_collection_update(org_id, col_id, headers, data, conn).await |     post_organization_collection_update(org_id, col_id, headers, data, conn).await | ||||||
| } | } | ||||||
|  |  | ||||||
| #[post("/organizations/<org_id>/collections/<col_id>", data = "<data>")] | #[post("/organizations/<org_id>/collections/<col_id>", data = "<data>", rank = 2)] | ||||||
| async fn post_organization_collection_update( | async fn post_organization_collection_update( | ||||||
|     org_id: OrganizationId, |     org_id: OrganizationId, | ||||||
|     col_id: CollectionId, |     col_id: CollectionId, | ||||||
|     headers: ManagerHeaders, |     headers: ManagerHeaders, | ||||||
|     data: Json<NewCollectionData>, |     data: Json<FullCollectionData>, | ||||||
|     mut conn: DbConn, |     mut conn: DbConn, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     if org_id != headers.org_id { |     if org_id != headers.org_id { | ||||||
|         err!("Organization not found", "Organization id's do not match"); |         err!("Organization not found", "Organization id's do not match"); | ||||||
|     } |     } | ||||||
|     let data: NewCollectionData = data.into_inner(); |     let data: FullCollectionData = data.into_inner(); | ||||||
|  |  | ||||||
|     if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() { |     if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() { | ||||||
|         err!("Can't find organization details") |         err!("Can't find organization details") | ||||||
| @@ -781,7 +857,7 @@ async fn get_collection_users( | |||||||
| async fn put_collection_users( | async fn put_collection_users( | ||||||
|     org_id: OrganizationId, |     org_id: OrganizationId, | ||||||
|     col_id: CollectionId, |     col_id: CollectionId, | ||||||
|     data: Json<Vec<MembershipData>>, |     data: Json<Vec<CollectionMembershipData>>, | ||||||
|     headers: ManagerHeaders, |     headers: ManagerHeaders, | ||||||
|     mut conn: DbConn, |     mut conn: DbConn, | ||||||
| ) -> EmptyResult { | ) -> EmptyResult { | ||||||
| @@ -913,24 +989,6 @@ async fn post_org_keys( | |||||||
|     }))) |     }))) | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| struct CollectionData { |  | ||||||
|     id: CollectionId, |  | ||||||
|     read_only: bool, |  | ||||||
|     hide_passwords: bool, |  | ||||||
|     manage: bool, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| #[serde(rename_all = "camelCase")] |  | ||||||
| struct MembershipData { |  | ||||||
|     id: MembershipId, |  | ||||||
|     read_only: bool, |  | ||||||
|     hide_passwords: bool, |  | ||||||
|     manage: bool, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct InviteData { | struct InviteData { | ||||||
| @@ -1754,7 +1812,7 @@ use super::ciphers::CipherData; | |||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct ImportData { | struct ImportData { | ||||||
|     ciphers: Vec<CipherData>, |     ciphers: Vec<CipherData>, | ||||||
|     collections: Vec<NewCollectionData>, |     collections: Vec<FullCollectionData>, | ||||||
|     collection_relationships: Vec<RelationsData>, |     collection_relationships: Vec<RelationsData>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -2549,7 +2607,7 @@ struct GroupRequest { | |||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     access_all: bool, |     access_all: bool, | ||||||
|     external_id: Option<String>, |     external_id: Option<String>, | ||||||
|     collections: Vec<SelectedCollection>, |     collections: Vec<CollectionData>, | ||||||
|     users: Vec<MembershipId>, |     users: Vec<MembershipId>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -2570,14 +2628,14 @@ impl GroupRequest { | |||||||
|  |  | ||||||
| #[derive(Deserialize, Serialize)] | #[derive(Deserialize, Serialize)] | ||||||
| #[serde(rename_all = "camelCase")] | #[serde(rename_all = "camelCase")] | ||||||
| struct SelectedCollection { | struct CollectionData { | ||||||
|     id: CollectionId, |     id: CollectionId, | ||||||
|     read_only: bool, |     read_only: bool, | ||||||
|     hide_passwords: bool, |     hide_passwords: bool, | ||||||
|     manage: bool, |     manage: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl SelectedCollection { | impl CollectionData { | ||||||
|     pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup { |     pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup { | ||||||
|         CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage) |         CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage) | ||||||
|     } |     } | ||||||
| @@ -2660,7 +2718,7 @@ async fn put_group( | |||||||
|  |  | ||||||
| async fn add_update_group( | async fn add_update_group( | ||||||
|     mut group: Group, |     mut group: Group, | ||||||
|     collections: Vec<SelectedCollection>, |     collections: Vec<CollectionData>, | ||||||
|     members: Vec<MembershipId>, |     members: Vec<MembershipId>, | ||||||
|     org_id: OrganizationId, |     org_id: OrganizationId, | ||||||
|     headers: &AdminHeaders, |     headers: &AdminHeaders, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user