diff --git a/src/api/admin.rs b/src/api/admin.rs
index d5656e98b68900bfea9765ee56043eecbff136f4..a25809edf45aa0f06425a14933534c6cf18e3593 100644
--- a/src/api/admin.rs
+++ b/src/api/admin.rs
@@ -660,7 +660,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
         "latest_release": latest_release,
         "latest_commit": latest_commit,
         "web_vault_enabled": &CONFIG.web_vault_enabled(),
-        "web_vault_version": web_vault_version.version,
+        "web_vault_version": web_vault_version.version.trim_start_matches('v'),
         "latest_web_build": latest_web_build,
         "running_within_docker": running_within_docker,
         "docker_base_image": if running_within_docker { docker_base_image() } else { "Not applicable" },
diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs
index 0354f46489adf78d0de5790fa2a5e6208f38bd7f..67633f0b8b266bad99a01d6ce75a05fb8f926b1d 100644
--- a/src/api/core/accounts.rs
+++ b/src/api/core/accounts.rs
@@ -841,6 +841,8 @@ async fn _api_key(
     headers: Headers,
     mut conn: DbConn,
 ) -> JsonResult {
+    use crate::util::format_date;
+
     let data: SecretVerificationRequest = data.into_inner().data;
     let mut user = headers.user;
 
@@ -855,6 +857,7 @@ async fn _api_key(
 
     Ok(Json(json!({
       "ApiKey": user.api_key,
+      "RevisionDate": format_date(&user.updated_at),
       "Object": "apiKey",
     })))
 }
diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs
index 0b0f10806cf3efe336be0fc5bbe3ecedfe605620..14d445978952a782ae79143c1aba11c638da8a03 100644
--- a/src/api/core/ciphers.rs
+++ b/src/api/core/ciphers.rs
@@ -56,7 +56,9 @@ pub fn routes() -> Vec<Route> {
         put_cipher_share,
         put_cipher_share_selected,
         post_cipher,
+        post_cipher_partial,
         put_cipher,
+        put_cipher_partial,
         delete_cipher_post,
         delete_cipher_post_admin,
         delete_cipher_put,
@@ -109,7 +111,10 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value>
     // Lets generate the ciphers_json using all the gathered info
     let mut ciphers_json = Vec::with_capacity(ciphers.len());
     for c in ciphers {
-        ciphers_json.push(c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &mut conn).await);
+        ciphers_json.push(
+            c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
+                .await,
+        );
     }
 
     let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await;
@@ -153,7 +158,10 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> {
 
     let mut ciphers_json = Vec::with_capacity(ciphers.len());
     for c in ciphers {
-        ciphers_json.push(c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &mut conn).await);
+        ciphers_json.push(
+            c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
+                .await,
+        );
     }
 
     Json(json!({
@@ -174,7 +182,7 @@ async fn get_cipher(uuid: String, headers: Headers, mut conn: DbConn) -> JsonRes
         err!("Cipher is not owned by user")
     }
 
-    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
 }
 
 #[get("/ciphers/<uuid>/admin")]
@@ -235,6 +243,13 @@ pub struct CipherData {
     LastKnownRevisionDate: Option<String>,
 }
 
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+pub struct PartialCipherData {
+    FolderId: Option<String>,
+    Favorite: bool,
+}
+
 #[derive(Deserialize, Debug)]
 #[allow(non_snake_case)]
 pub struct Attachments2Data {
@@ -314,7 +329,7 @@ async fn post_ciphers(
     update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherCreate)
         .await?;
 
-    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
 }
 
 /// Enforces the personal ownership policy on user-owned ciphers, if applicable.
@@ -646,7 +661,51 @@ async fn put_cipher(
     update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherUpdate)
         .await?;
 
-    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
+}
+
+#[post("/ciphers/<uuid>/partial", data = "<data>")]
+async fn post_cipher_partial(
+    uuid: String,
+    data: JsonUpcase<PartialCipherData>,
+    headers: Headers,
+    conn: DbConn,
+) -> JsonResult {
+    put_cipher_partial(uuid, data, headers, conn).await
+}
+
+// Only update the folder and favorite for the user, since this cipher is read-only
+#[put("/ciphers/<uuid>/partial", data = "<data>")]
+async fn put_cipher_partial(
+    uuid: String,
+    data: JsonUpcase<PartialCipherData>,
+    headers: Headers,
+    mut conn: DbConn,
+) -> JsonResult {
+    let data: PartialCipherData = data.into_inner().data;
+
+    let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
+        Some(cipher) => cipher,
+        None => err!("Cipher doesn't exist"),
+    };
+
+    if let Some(ref folder_id) = data.FolderId {
+        match Folder::find_by_uuid(folder_id, &mut conn).await {
+            Some(folder) => {
+                if folder.user_uuid != headers.user.uuid {
+                    err!("Folder is not owned by user")
+                }
+            }
+            None => err!("Folder doesn't exist"),
+        }
+    }
+
+    // Move cipher
+    cipher.move_to_folder(data.FolderId.clone(), &headers.user.uuid, &mut conn).await?;
+    // Update favorite
+    cipher.set_favorite(Some(data.Favorite), &headers.user.uuid, &mut conn).await?;
+
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
 }
 
 #[derive(Deserialize)]
@@ -873,7 +932,7 @@ async fn share_cipher_by_uuid(
 
     update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?;
 
-    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await))
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
 }
 
 /// v2 API for downloading an attachment. This just redirects the client to
@@ -942,7 +1001,7 @@ async fn post_attachment_v2(
         "AttachmentId": attachment_id,
         "Url": url,
         "FileUploadType": FileUploadType::Direct as i32,
-        response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await,
+        response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await,
     })))
 }
 
@@ -1135,7 +1194,7 @@ async fn post_attachment(
 
     let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?;
 
-    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
 }
 
 #[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
@@ -1616,7 +1675,7 @@ async fn _restore_cipher_by_uuid(
         .await;
     }
 
-    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await))
+    Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
 }
 
 async fn _restore_multiple_ciphers(
@@ -1716,6 +1775,7 @@ pub struct CipherSyncData {
     pub user_group_full_access_for_organizations: HashSet<String>,
 }
 
+#[derive(Eq, PartialEq)]
 pub enum CipherSyncType {
     User,
     Organization,
diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs
index 1730de01babcf9f045973cda6a324cf466067d87..dd0617e27585e0f62302a691782d247ec9d374b6 100644
--- a/src/api/core/emergency_access.rs
+++ b/src/api/core/emergency_access.rs
@@ -590,8 +590,16 @@ async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbCo
 
     let mut ciphers_json = Vec::with_capacity(ciphers.len());
     for c in ciphers {
-        ciphers_json
-            .push(c.to_json(&headers.host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await);
+        ciphers_json.push(
+            c.to_json(
+                &headers.host,
+                &emergency_access.grantor_uuid,
+                Some(&cipher_sync_data),
+                CipherSyncType::User,
+                &mut conn,
+            )
+            .await,
+        );
     }
 
     Ok(Json(json!({
diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs
index d029cb60fb30e8fa9fde7f07ebc1545eaf60808f..6a483842a04778b809de541fa35a414bd5355792 100644
--- a/src/api/core/mod.rs
+++ b/src/api/core/mod.rs
@@ -237,6 +237,7 @@ fn config() -> Json<Value> {
           "notifications": format!("{domain}/notifications"),
           "sso": "",
         },
+        "object": "config",
     }))
 }
 
diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs
index 38ff63b7cfb97b1206e4cb0e6135ba3a89d3848d..1353e61b865c7fdd9d38392afc6478100a61f654 100644
--- a/src/api/core/organizations.rs
+++ b/src/api/core/organizations.rs
@@ -118,12 +118,13 @@ struct OrganizationUpdateData {
 #[allow(non_snake_case)]
 struct NewCollectionData {
     Name: String,
-    Groups: Vec<NewCollectionGroupData>,
+    Groups: Vec<NewCollectionObjectData>,
+    Users: Vec<NewCollectionObjectData>,
 }
 
 #[derive(Deserialize)]
 #[allow(non_snake_case)]
-struct NewCollectionGroupData {
+struct NewCollectionObjectData {
     HidePasswords: bool,
     Id: String,
     ReadOnly: bool,
@@ -311,29 +312,62 @@ async fn get_org_collections(org_id: String, _headers: ManagerHeadersLoose, mut
 }
 
 #[get("/organizations/<org_id>/collections/details")]
-async fn get_org_collections_details(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
+async fn get_org_collections_details(org_id: String, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
     let mut data = Vec::new();
 
+    let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
+        Some(u) => u,
+        None => err!("User is not part of organization"),
+    };
+
+    let coll_users = CollectionUser::find_by_organization(&org_id, &mut conn).await;
+
     for col in Collection::find_by_organization(&org_id, &mut conn).await {
-        let groups: Vec<Value> = CollectionGroup::find_by_collection(&col.uuid, &mut conn)
-            .await
+        let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
+            CollectionGroup::find_by_collection(&col.uuid, &mut conn)
+                .await
+                .iter()
+                .map(|collection_group| {
+                    SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
+                })
+                .collect()
+        } else {
+            // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
+            // so just act as if there are no groups.
+            Vec::with_capacity(0)
+        };
+
+        let mut assigned = false;
+        let users: Vec<Value> = coll_users
             .iter()
-            .map(|collection_group| {
-                SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
+            .filter(|collection_user| collection_user.collection_uuid == col.uuid)
+            .map(|collection_user| {
+                // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `CollectionUser::find_by_organization` call.
+                // We check here if the current user is assigned to this collection or not.
+                if collection_user.user_uuid == user_org.uuid {
+                    assigned = true;
+                }
+                SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
             })
             .collect();
 
+        if user_org.access_all {
+            assigned = true;
+        }
+
         let mut json_object = col.to_json();
+        json_object["Assigned"] = json!(assigned);
+        json_object["Users"] = json!(users);
         json_object["Groups"] = json!(groups);
-        json_object["Object"] = json!("collectionGroupDetails");
+        json_object["Object"] = json!("collectionAccessDetails");
         data.push(json_object)
     }
 
-    Json(json!({
+    Ok(Json(json!({
         "Data": data,
         "Object": "list",
         "ContinuationToken": null,
-    }))
+    })))
 }
 
 async fn _get_org_collections(org_id: &str, conn: &mut DbConn) -> Value {
@@ -355,12 +389,6 @@ async fn post_organization_collections(
         None => err!("Can't find organization details"),
     };
 
-    // Get the user_organization record so that we can check if the user has access to all collections.
-    let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
-        Some(u) => u,
-        None => err!("User is not part of organization"),
-    };
-
     let collection = Collection::new(org.uuid, data.Name);
     collection.save(&mut conn).await?;
 
@@ -381,11 +409,18 @@ async fn post_organization_collections(
             .await?;
     }
 
-    // If the user doesn't have access to all collections, only in case of a Manger,
-    // then we need to save the creating user uuid (Manager) to the users_collection table.
-    // Else the user will not have access to his own created collection.
-    if !user_org.access_all {
-        CollectionUser::save(&headers.user.uuid, &collection.uuid, false, false, &mut conn).await?;
+    for user in data.Users {
+        let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await {
+            Some(u) => u,
+            None => err!("User is not part of organization"),
+        };
+
+        if org_user.access_all {
+            continue;
+        }
+
+        CollectionUser::save(&org_user.user_uuid, &collection.uuid, user.ReadOnly, user.HidePasswords, &mut conn)
+            .await?;
     }
 
     Ok(Json(collection.to_json()))
@@ -448,6 +483,21 @@ async fn post_organization_collection_update(
         CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&mut conn).await?;
     }
 
+    CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
+
+    for user in data.Users {
+        let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await {
+            Some(u) => u,
+            None => err!("User is not part of organization"),
+        };
+
+        if org_user.access_all {
+            continue;
+        }
+
+        CollectionUser::save(&org_user.user_uuid, &col_id, user.ReadOnly, user.HidePasswords, &mut conn).await?;
+    }
+
     Ok(Json(collection.to_json()))
 }
 
@@ -555,17 +605,49 @@ async fn get_org_collection_detail(
                 err!("Collection is not owned by organization")
             }
 
-            let groups: Vec<Value> = CollectionGroup::find_by_collection(&collection.uuid, &mut conn)
-                .await
-                .iter()
-                .map(|collection_group| {
-                    SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
-                })
-                .collect();
+            let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
+                Some(u) => u,
+                None => err!("User is not part of organization"),
+            };
+
+            let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
+                CollectionGroup::find_by_collection(&collection.uuid, &mut conn)
+                    .await
+                    .iter()
+                    .map(|collection_group| {
+                        SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
+                    })
+                    .collect()
+            } else {
+                // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
+                // so just act as if there are no groups.
+                Vec::with_capacity(0)
+            };
+
+            let mut assigned = false;
+            let users: Vec<Value> =
+                CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn)
+                    .await
+                    .iter()
+                    .map(|collection_user| {
+                        // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `find_by_collection_swap_user_uuid_with_org_user_uuid` call.
+                        // We check here if the current user is assigned to this collection or not.
+                        if collection_user.user_uuid == user_org.uuid {
+                            assigned = true;
+                        }
+                        SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
+                    })
+                    .collect();
+
+            if user_org.access_all {
+                assigned = true;
+            }
 
             let mut json_object = collection.to_json();
+            json_object["Assigned"] = json!(assigned);
+            json_object["Users"] = json!(users);
             json_object["Groups"] = json!(groups);
-            json_object["Object"] = json!("collectionGroupDetails");
+            json_object["Object"] = json!("collectionAccessDetails");
 
             Ok(Json(json_object))
         }
@@ -652,16 +734,39 @@ async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut
 
     let mut ciphers_json = Vec::with_capacity(ciphers.len());
     for c in ciphers {
-        ciphers_json.push(c.to_json(host, user_uuid, Some(&cipher_sync_data), conn).await);
+        ciphers_json
+            .push(c.to_json(host, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await);
     }
     json!(ciphers_json)
 }
 
-#[get("/organizations/<org_id>/users")]
-async fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
+#[derive(FromForm)]
+struct GetOrgUserData {
+    #[field(name = "includeCollections")]
+    include_collections: Option<bool>,
+    #[field(name = "includeGroups")]
+    include_groups: Option<bool>,
+}
+
+// includeCollections
+// includeGroups
+#[get("/organizations/<org_id>/users?<data..>")]
+async fn get_org_users(
+    data: GetOrgUserData,
+    org_id: String,
+    _headers: ManagerHeadersLoose,
+    mut conn: DbConn,
+) -> Json<Value> {
     let mut users_json = Vec::new();
     for u in UserOrganization::find_by_org(&org_id, &mut conn).await {
-        users_json.push(u.to_json_user_details(&mut conn).await);
+        users_json.push(
+            u.to_json_user_details(
+                data.include_collections.unwrap_or(false),
+                data.include_groups.unwrap_or(false),
+                &mut conn,
+            )
+            .await,
+        );
     }
 
     Json(json!({
@@ -2056,12 +2161,18 @@ async fn _restore_organization_user(
 
 #[get("/organizations/<org_id>/groups")]
 async fn get_groups(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
-    let groups = if CONFIG.org_groups_enabled() {
-        Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
+    let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
+        // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
+        let groups = Group::find_by_organization(&org_id, &mut conn).await;
+        let mut groups_json = Vec::with_capacity(groups.len());
+        for g in groups {
+            groups_json.push(g.to_json_details(&mut conn).await)
+        }
+        groups_json
     } else {
         // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
         // so just act as if there are no groups.
-        Value::Array(Vec::new())
+        Vec::with_capacity(0)
     };
 
     Ok(Json(json!({
@@ -2078,6 +2189,7 @@ struct GroupRequest {
     AccessAll: Option<bool>,
     ExternalId: Option<String>,
     Collections: Vec<SelectionReadOnly>,
+    Users: Vec<String>,
 }
 
 impl GroupRequest {
@@ -2120,19 +2232,19 @@ impl SelectionReadOnly {
         CollectionGroup::new(self.Id.clone(), groups_uuid, self.ReadOnly, self.HidePasswords)
     }
 
-    pub fn to_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
+    pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
         SelectionReadOnly {
-            Id: collection_group.collections_uuid.clone(),
+            Id: collection_group.groups_uuid.clone(),
             ReadOnly: collection_group.read_only,
             HidePasswords: collection_group.hide_passwords,
         }
     }
 
-    pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
+    pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly {
         SelectionReadOnly {
-            Id: collection_group.groups_uuid.clone(),
-            ReadOnly: collection_group.read_only,
-            HidePasswords: collection_group.hide_passwords,
+            Id: collection_user.user_uuid.clone(),
+            ReadOnly: collection_user.read_only,
+            HidePasswords: collection_user.hide_passwords,
         }
     }
 
@@ -2171,7 +2283,7 @@ async fn post_groups(
     log_event(
         EventType::GroupCreated as i32,
         &group.uuid,
-        org_id,
+        org_id.clone(),
         headers.user.uuid.clone(),
         headers.device.atype,
         &ip.ip,
@@ -2179,7 +2291,7 @@ async fn post_groups(
     )
     .await;
 
-    add_update_group(group, group_request.Collections, &mut conn).await
+    add_update_group(group, group_request.Collections, group_request.Users, &org_id, &headers, &ip, &mut conn).await
 }
 
 #[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
@@ -2204,11 +2316,12 @@ async fn put_group(
     let updated_group = group_request.update_group(group)?;
 
     CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?;
+    GroupUser::delete_all_by_group(&group_id, &mut conn).await?;
 
     log_event(
         EventType::GroupUpdated as i32,
         &updated_group.uuid,
-        org_id,
+        org_id.clone(),
         headers.user.uuid.clone(),
         headers.device.atype,
         &ip.ip,
@@ -2216,18 +2329,42 @@ async fn put_group(
     )
     .await;
 
-    add_update_group(updated_group, group_request.Collections, &mut conn).await
+    add_update_group(updated_group, group_request.Collections, group_request.Users, &org_id, &headers, &ip, &mut conn)
+        .await
 }
 
-async fn add_update_group(mut group: Group, collections: Vec<SelectionReadOnly>, conn: &mut DbConn) -> JsonResult {
+async fn add_update_group(
+    mut group: Group,
+    collections: Vec<SelectionReadOnly>,
+    users: Vec<String>,
+    org_id: &str,
+    headers: &AdminHeaders,
+    ip: &ClientIp,
+    conn: &mut DbConn,
+) -> JsonResult {
     group.save(conn).await?;
 
     for selection_read_only_request in collections {
         let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone());
-
         collection_group.save(conn).await?;
     }
 
+    for assigned_user_id in users {
+        let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_user_id.clone());
+        user_entry.save(conn).await?;
+
+        log_event(
+            EventType::OrganizationUserUpdatedGroups as i32,
+            &assigned_user_id,
+            String::from(org_id),
+            headers.user.uuid.clone(),
+            headers.device.atype,
+            &ip.ip,
+            conn,
+        )
+        .await;
+    }
+
     Ok(Json(json!({
         "Id": group.uuid,
         "OrganizationId": group.organizations_uuid,
@@ -2248,20 +2385,7 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea
         _ => err!("Group could not be found!"),
     };
 
-    let collections_groups = CollectionGroup::find_by_group(&group_id, &mut conn)
-        .await
-        .iter()
-        .map(|entry| SelectionReadOnly::to_group_details_read_only(entry).to_json())
-        .collect::<Value>();
-
-    Ok(Json(json!({
-        "Id": group.uuid,
-        "OrganizationId": group.organizations_uuid,
-        "Name": group.name,
-        "AccessAll": group.access_all,
-        "ExternalId": group.get_external_id(),
-        "Collections": collections_groups
-    })))
+    Ok(Json(group.to_json_details(&mut conn).await))
 }
 
 #[post("/organizations/<org_id>/groups/<group_id>/delete")]
diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs
index b7d26bd3d7c0122526620af59d1285f66c0848da..79212f6abb5b3fba10178fb481f35d5a24b9dc26 100644
--- a/src/db/models/cipher.rs
+++ b/src/db/models/cipher.rs
@@ -6,7 +6,7 @@ use super::{
     Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
 };
 
-use crate::api::core::{CipherData, CipherSyncData};
+use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
 
 use std::borrow::Cow;
 
@@ -114,6 +114,7 @@ impl Cipher {
         host: &str,
         user_uuid: &str,
         cipher_sync_data: Option<&CipherSyncData>,
+        sync_type: CipherSyncType,
         conn: &mut DbConn,
     ) -> Value {
         use crate::util::format_date;
@@ -134,12 +135,18 @@ impl Cipher {
         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, cipher_sync_data, conn).await {
-            Some((ro, hp)) => (ro, hp),
-            None => {
-                error!("Cipher ownership assertion failure");
-                (true, true)
+        // We don't need these values at all for Organizational syncs
+        // Skip any other database calls if this is the case and just return false.
+        let (read_only, hide_passwords) = if sync_type == CipherSyncType::User {
+            match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
+                Some((ro, hp)) => (ro, hp),
+                None => {
+                    error!("Cipher ownership assertion failure");
+                    (true, true)
+                }
             }
+        } else {
+            (false, false)
         };
 
         // Get the type_data or a default to an empty json object '{}'.
@@ -192,8 +199,6 @@ impl Cipher {
             "CreationDate": format_date(&self.created_at),
             "RevisionDate": format_date(&self.updated_at),
             "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
-            "FolderId": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string() ) } else { self.get_folder_uuid(user_uuid, conn).await },
-            "Favorite": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_favorites.contains(&self.uuid) } else { self.is_favorite(user_uuid, conn).await },
             "Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32),
             "OrganizationId": self.organization_uuid,
             "Attachments": attachments_json,
@@ -210,12 +215,6 @@ impl Cipher {
 
             "Data": data_json,
 
-            // These values are true by default, but can be false if the
-            // 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,
 
             // All Cipher types are included by default as null, but only the matching one will be populated
@@ -225,6 +224,27 @@ impl Cipher {
             "Identity": null,
         });
 
+        // These values are only needed for user/default syncs
+        // Not during an organizational sync like `get_org_details`
+        // Skip adding these fields in that case
+        if sync_type == CipherSyncType::User {
+            json_object["FolderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
+                cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string())
+            } else {
+                self.get_folder_uuid(user_uuid, conn).await
+            });
+            json_object["Favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
+                cipher_sync_data.cipher_favorites.contains(&self.uuid)
+            } else {
+                self.is_favorite(user_uuid, conn).await
+            });
+            // These values are true by default, but can be false if the
+            // cipher belongs to a collection or group where the org owner has enabled
+            // the "Read Only" or "Hide Passwords" restrictions for the user.
+            json_object["Edit"] = json!(!read_only);
+            json_object["ViewPassword"] = json!(!hide_passwords);
+        }
+
         let key = match self.atype {
             1 => "Login",
             2 => "SecureNote",
@@ -740,6 +760,7 @@ impl Cipher {
             .or_filter(groups::access_all.eq(true)) //Access via group
             .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
             .select(ciphers_collections::all_columns)
+            .distinct()
             .load::<(String, String)>(conn).unwrap_or_default()
         }}
     }
diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs
index 68d59a20a80070a32b63c549f41651638373a75d..6a9acab84b3378a97662eb494459f073a0b8b557 100644
--- a/src/db/models/collection.rs
+++ b/src/db/models/collection.rs
@@ -407,6 +407,19 @@ impl CollectionUser {
         }}
     }
 
+    pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            users_collections::table
+                .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
+                .filter(collections::org_uuid.eq(org_uuid))
+                .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
+                .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords))
+                .load::<CollectionUserDb>(conn)
+                .expect("Error loading users_collections")
+                .from_db()
+        }}
+    }
+
     pub async fn save(
         user_uuid: &str,
         collection_uuid: &str,
@@ -490,6 +503,21 @@ impl CollectionUser {
         }}
     }
 
+    pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid(
+        collection_uuid: &str,
+        conn: &mut DbConn,
+    ) -> Vec<Self> {
+        db_run! { conn: {
+            users_collections::table
+                .filter(users_collections::collection_uuid.eq(collection_uuid))
+                .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
+                .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords))
+                .load::<CollectionUserDb>(conn)
+                .expect("Error loading users_collections")
+                .from_db()
+        }}
+    }
+
     pub async fn find_by_collection_and_user(
         collection_uuid: &str,
         user_uuid: &str,
diff --git a/src/db/models/group.rs b/src/db/models/group.rs
index e5919612e3e35bf4085fc09065de75e41a6b7697..258b9e42a154d6f5699b20d9ea4e5e245404cebd 100644
--- a/src/db/models/group.rs
+++ b/src/db/models/group.rs
@@ -64,7 +64,32 @@ impl Group {
             "AccessAll": self.access_all,
             "ExternalId": self.external_id,
             "CreationDate": format_date(&self.creation_date),
-            "RevisionDate": format_date(&self.revision_date)
+            "RevisionDate": format_date(&self.revision_date),
+            "Object": "group"
+        })
+    }
+
+    pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
+        let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
+            .await
+            .iter()
+            .map(|entry| {
+                json!({
+                    "Id": entry.collections_uuid,
+                    "ReadOnly": entry.read_only,
+                    "HidePasswords": entry.hide_passwords
+                })
+            })
+            .collect();
+
+        json!({
+            "Id": self.uuid,
+            "OrganizationId": self.organizations_uuid,
+            "Name": self.name,
+            "AccessAll": self.access_all,
+            "ExternalId": self.external_id,
+            "Collections": collections_groups,
+            "Object": "groupDetails"
         })
     }
 
diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs
index 34325b787b77e781aa79b2766664c4d9c0d0215f..6010c20725a563f421702ffb270cf8c4daf523a5 100644
--- a/src/db/models/organization.rs
+++ b/src/db/models/organization.rs
@@ -326,7 +326,7 @@ impl UserOrganization {
             // TODO: Add support for Custom User Roles
             // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
             // "Permissions": {
-            //     "AccessEventLogs": false, // Not supported
+            //     "AccessEventLogs": false,
             //     "AccessImportExport": false,
             //     "AccessReports": false,
             //     "ManageAllCollections": false,
@@ -337,9 +337,9 @@ impl UserOrganization {
             //     "editAssignedCollections": false,
             //     "deleteAssignedCollections": false,
             //     "ManageCiphers": false,
-            //     "ManageGroups": false, // Not supported
+            //     "ManageGroups": false,
             //     "ManagePolicies": false,
-            //     "ManageResetPassword": false, // Not supported
+            //     "ManageResetPassword": false,
             //     "ManageSso": false, // Not supported
             //     "ManageUsers": false,
             //     "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
@@ -358,7 +358,12 @@ impl UserOrganization {
         })
     }
 
-    pub async fn to_json_user_details(&self, conn: &mut DbConn) -> Value {
+    pub async fn to_json_user_details(
+        &self,
+        include_collections: bool,
+        include_groups: bool,
+        conn: &mut DbConn,
+    ) -> Value {
         let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
 
         // Because BitWarden want the status to be -1 for revoked users we need to catch that here.
@@ -371,11 +376,37 @@ impl UserOrganization {
 
         let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty();
 
+        let groups: Vec<String> = if include_groups && CONFIG.org_groups_enabled() {
+            GroupUser::find_by_user(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect()
+        } else {
+            // The Bitwarden clients seem to call this API regardless of whether groups are enabled,
+            // so just act as if there are no groups.
+            Vec::with_capacity(0)
+        };
+
+        let collections: Vec<Value> = if include_collections {
+            CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
+                .await
+                .iter()
+                .map(|cu| {
+                    json!({
+                        "Id": cu.collection_uuid,
+                        "ReadOnly": cu.read_only,
+                        "HidePasswords": cu.hide_passwords,
+                    })
+                })
+                .collect()
+        } else {
+            Vec::with_capacity(0)
+        };
+
         json!({
             "Id": self.uuid,
             "UserId": self.user_uuid,
             "Name": user.name,
             "Email": user.email,
+            "Groups": groups,
+            "Collections": collections,
 
             "Status": status,
             "Type": self.atype,