add bulk-access endpoint for collections (#5542)

This commit is contained in:
Stefan Melmuk 2025-02-04 09:42:02 +01:00 committed by GitHub
parent d2b36642a6
commit 3b6bccde97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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,