diff --git a/migrations/mysql/2022-07-27-110000_add_group_support/down.sql b/migrations/mysql/2022-07-27-110000_add_group_support/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..0ec5c501a0eb4388f5854c969c19e4c8a48f6149
--- /dev/null
+++ b/migrations/mysql/2022-07-27-110000_add_group_support/down.sql
@@ -0,0 +1,3 @@
+DROP TABLE `groups`;
+DROP TABLE groups_users;
+DROP TABLE collections_groups;
\ No newline at end of file
diff --git a/migrations/mysql/2022-07-27-110000_add_group_support/up.sql b/migrations/mysql/2022-07-27-110000_add_group_support/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..6d40638ab1db7428c588007994d4751ac2841ce9
--- /dev/null
+++ b/migrations/mysql/2022-07-27-110000_add_group_support/up.sql
@@ -0,0 +1,23 @@
+CREATE TABLE `groups` (
+  uuid                              CHAR(36) NOT NULL PRIMARY KEY,
+  organizations_uuid                VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
+  name                              VARCHAR(100) NOT NULL,
+  access_all                        BOOLEAN NOT NULL,
+  external_id                       VARCHAR(300) NULL,
+  creation_date                     DATETIME NOT NULL,
+  revision_date                     DATETIME NOT NULL
+);
+
+CREATE TABLE groups_users (
+  groups_uuid                       CHAR(36) NOT NULL REFERENCES `groups` (uuid),
+  users_organizations_uuid          VARCHAR(36) NOT NULL REFERENCES users_organizations (uuid),
+  UNIQUE (groups_uuid, users_organizations_uuid)
+);
+
+CREATE TABLE collections_groups (
+  collections_uuid                  VARCHAR(40) NOT NULL REFERENCES collections (uuid),
+  groups_uuid                       CHAR(36) NOT NULL REFERENCES `groups` (uuid),
+  read_only                         BOOLEAN NOT NULL,
+  hide_passwords                    BOOLEAN NOT NULL,
+  UNIQUE (collections_uuid, groups_uuid)
+);
\ No newline at end of file
diff --git a/migrations/postgresql/2022-07-27-110000_add_group_support/down.sql b/migrations/postgresql/2022-07-27-110000_add_group_support/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..9a12d694ac3693cf0233b98b167646ed104258f1
--- /dev/null
+++ b/migrations/postgresql/2022-07-27-110000_add_group_support/down.sql
@@ -0,0 +1,3 @@
+DROP TABLE groups;
+DROP TABLE groups_users;
+DROP TABLE collections_groups;
\ No newline at end of file
diff --git a/migrations/postgresql/2022-07-27-110000_add_group_support/up.sql b/migrations/postgresql/2022-07-27-110000_add_group_support/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..5eed1df39651bab147aba1ac763c41c5f34ea9e4
--- /dev/null
+++ b/migrations/postgresql/2022-07-27-110000_add_group_support/up.sql
@@ -0,0 +1,23 @@
+CREATE TABLE groups (
+  uuid                              CHAR(36) NOT NULL PRIMARY KEY,
+  organizations_uuid                 VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
+  name                              VARCHAR(100) NOT NULL,
+  access_all                        BOOLEAN NOT NULL,
+  external_id                       VARCHAR(300) NULL,
+  creation_date                     TIMESTAMP NOT NULL,
+  revision_date                     TIMESTAMP NOT NULL
+);
+
+CREATE TABLE groups_users (
+  groups_uuid                       CHAR(36) NOT NULL REFERENCES groups (uuid),
+  users_organizations_uuid          VARCHAR(36) NOT NULL REFERENCES users_organizations (uuid),
+  PRIMARY KEY (groups_uuid, users_organizations_uuid)
+);
+
+CREATE TABLE collections_groups (
+  collections_uuid                  VARCHAR(40) NOT NULL REFERENCES collections (uuid),
+  groups_uuid                       CHAR(36) NOT NULL REFERENCES groups (uuid),
+  read_only                         BOOLEAN NOT NULL,
+  hide_passwords                    BOOLEAN NOT NULL,
+  PRIMARY KEY (collections_uuid, groups_uuid)
+);
\ No newline at end of file
diff --git a/migrations/sqlite/2022-07-27-110000_add_group_support/down.sql b/migrations/sqlite/2022-07-27-110000_add_group_support/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..9a12d694ac3693cf0233b98b167646ed104258f1
--- /dev/null
+++ b/migrations/sqlite/2022-07-27-110000_add_group_support/down.sql
@@ -0,0 +1,3 @@
+DROP TABLE groups;
+DROP TABLE groups_users;
+DROP TABLE collections_groups;
\ No newline at end of file
diff --git a/migrations/sqlite/2022-07-27-110000_add_group_support/up.sql b/migrations/sqlite/2022-07-27-110000_add_group_support/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..0523c7604b29ebc0ef2667f3c7512c51a4f56241
--- /dev/null
+++ b/migrations/sqlite/2022-07-27-110000_add_group_support/up.sql
@@ -0,0 +1,23 @@
+CREATE TABLE groups (
+  uuid                              TEXT NOT NULL PRIMARY KEY,
+  organizations_uuid                TEXT NOT NULL REFERENCES organizations (uuid),
+  name                              TEXT NOT NULL,
+  access_all                        BOOLEAN NOT NULL,
+  external_id                       TEXT NULL,
+  creation_date                     TIMESTAMP NOT NULL,
+  revision_date                     TIMESTAMP NOT NULL
+);
+
+CREATE TABLE groups_users (
+  groups_uuid                       TEXT NOT NULL REFERENCES groups (uuid),
+  users_organizations_uuid          TEXT NOT NULL REFERENCES users_organizations (uuid),
+  UNIQUE (groups_uuid, users_organizations_uuid)
+);
+
+CREATE TABLE collections_groups (
+  collections_uuid                  TEXT NOT NULL REFERENCES collections (uuid),
+  groups_uuid                       TEXT NOT NULL REFERENCES groups (uuid),
+  read_only                         BOOLEAN NOT NULL,
+  hide_passwords                    BOOLEAN NOT NULL,
+  UNIQUE (collections_uuid, groups_uuid)
+);
\ No newline at end of file
diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs
index 1870d12bf09530206d4942ca4fa3b664f421e79c..051d947608e6538e20107b9e78560a1ae23785f4 100644
--- a/src/api/core/ciphers.rs
+++ b/src/api/core/ciphers.rs
@@ -1499,6 +1499,8 @@ pub struct CipherSyncData {
     pub cipher_collections: HashMap<String, Vec<String>>,
     pub user_organizations: HashMap<String, UserOrganization>,
     pub user_collections: HashMap<String, CollectionUser>,
+    pub user_collections_groups: HashMap<String, CollectionGroup>,
+    pub user_group_full_access_for_organizations: HashSet<String>,
 }
 
 pub enum CipherSyncType {
@@ -1554,6 +1556,16 @@ impl CipherSyncData {
                 .collect()
                 .await;
 
+        // Generate a HashMap with the collections_uuid as key and the CollectionGroup record
+        let user_collections_groups = stream::iter(CollectionGroup::find_by_user(user_uuid, conn).await)
+            .map(|collection_group| (collection_group.collections_uuid.clone(), collection_group))
+            .collect()
+            .await;
+
+        // Get all organizations that the user has full access to via group assignement
+        let user_group_full_access_for_organizations =
+            stream::iter(Group::gather_user_organizations_full_access(user_uuid, conn).await).collect().await;
+
         Self {
             cipher_attachments,
             cipher_folders,
@@ -1561,6 +1573,8 @@ impl CipherSyncData {
             cipher_collections,
             user_organizations,
             user_collections,
+            user_collections_groups,
+            user_group_full_access_for_organizations,
         }
     }
 }
diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs
index 9f2178e7b886805db1686e4dc97a4d866d2b09ff..07baa7ba5ebaed823f082a2ecb251750bd533b16 100644
--- a/src/api/core/organizations.rs
+++ b/src/api/core/organizations.rs
@@ -6,7 +6,8 @@ use serde_json::Value;
 use crate::{
     api::{
         core::{CipherSyncData, CipherSyncType},
-        EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType,
+        ApiResult, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordData,
+        UpdateType,
     },
     auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
     db::{models::*, DbConn},
@@ -71,6 +72,21 @@ pub fn routes() -> Vec<Route> {
         bulk_activate_organization_user,
         restore_organization_user,
         bulk_restore_organization_user,
+        get_groups,
+        post_groups,
+        get_group,
+        put_group,
+        post_group,
+        get_group_details,
+        delete_group,
+        post_delete_group,
+        get_group_users,
+        put_group_users,
+        get_user_groups,
+        post_user_groups,
+        put_user_groups,
+        delete_group_user,
+        post_delete_group_user,
         get_org_export
     ]
 }
@@ -94,10 +110,19 @@ struct OrganizationUpdateData {
     Name: String,
 }
 
-#[derive(Deserialize, Debug)]
+#[derive(Deserialize)]
 #[allow(non_snake_case)]
 struct NewCollectionData {
     Name: String,
+    Groups: Vec<NewCollectionGroupData>,
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct NewCollectionGroupData {
+    HidePasswords: bool,
+    Id: String,
+    ReadOnly: bool,
 }
 
 #[derive(Deserialize)]
@@ -287,6 +312,12 @@ async fn post_organization_collections(
     let collection = Collection::new(org.uuid, data.Name);
     collection.save(&conn).await?;
 
+    for group in data.Groups {
+        CollectionGroup::new(collection.uuid.clone(), group.Id, group.ReadOnly, group.HidePasswords)
+            .save(&conn)
+            .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.
@@ -335,6 +366,12 @@ async fn post_organization_collection_update(
     collection.name = data.Name;
     collection.save(&conn).await?;
 
+    CollectionGroup::delete_all_by_collection(&col_id, &conn).await?;
+
+    for group in data.Groups {
+        CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&conn).await?;
+    }
+
     Ok(Json(collection.to_json()))
 }
 
@@ -430,7 +467,19 @@ async fn get_org_collection_detail(
                 err!("Collection is not owned by organization")
             }
 
-            Ok(Json(collection.to_json()))
+            let groups: Vec<Value> = CollectionGroup::find_by_collection(&collection.uuid, &conn)
+                .await
+                .iter()
+                .map(|collection_group| {
+                    SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
+                })
+                .collect();
+
+            let mut json_object = collection.to_json();
+            json_object["Groups"] = json!(groups);
+            json_object["Object"] = json!("collectionGroupDetails");
+
+            Ok(Json(json_object))
         }
     }
 }
@@ -1704,6 +1753,324 @@ async fn _restore_organization_user(
     Ok(())
 }
 
+#[get("/organizations/<org_id>/groups")]
+async fn get_groups(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
+    let groups = Group::find_by_organization(&org_id, &conn).await.iter().map(Group::to_json).collect::<Value>();
+
+    Ok(Json(json!({
+        "Data": groups,
+        "Object": "list",
+        "ContinuationToken": null,
+    })))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct GroupRequest {
+    Name: String,
+    AccessAll: Option<bool>,
+    ExternalId: Option<String>,
+    Collections: Vec<SelectionReadOnly>,
+}
+
+impl GroupRequest {
+    pub fn to_group(&self, organizations_uuid: &str) -> ApiResult<Group> {
+        match self.AccessAll {
+            Some(access_all_value) => Ok(Group::new(
+                organizations_uuid.to_owned(),
+                self.Name.clone(),
+                access_all_value,
+                self.ExternalId.clone(),
+            )),
+            _ => err!("Could not convert GroupRequest to Group, because AccessAll has no value!"),
+        }
+    }
+
+    pub fn update_group(&self, mut group: Group) -> ApiResult<Group> {
+        match self.AccessAll {
+            Some(access_all_value) => {
+                group.name = self.Name.clone();
+                group.access_all = access_all_value;
+                group.set_external_id(self.ExternalId.clone());
+
+                Ok(group)
+            }
+            _ => err!("Could not update group, because AccessAll has no value!"),
+        }
+    }
+}
+
+#[derive(Deserialize, Serialize)]
+#[allow(non_snake_case)]
+struct SelectionReadOnly {
+    Id: String,
+    ReadOnly: bool,
+    HidePasswords: bool,
+}
+
+impl SelectionReadOnly {
+    pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup {
+        CollectionGroup::new(self.Id.clone(), groups_uuid, self.ReadOnly, self.HidePasswords)
+    }
+
+    pub fn to_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
+        SelectionReadOnly {
+            Id: collection_group.collections_uuid.clone(),
+            ReadOnly: collection_group.read_only,
+            HidePasswords: collection_group.hide_passwords,
+        }
+    }
+
+    pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
+        SelectionReadOnly {
+            Id: collection_group.groups_uuid.clone(),
+            ReadOnly: collection_group.read_only,
+            HidePasswords: collection_group.hide_passwords,
+        }
+    }
+
+    pub fn to_json(&self) -> Value {
+        json!(self)
+    }
+}
+
+#[post("/organizations/<_org_id>/groups/<group_id>", data = "<data>")]
+async fn post_group(
+    _org_id: String,
+    group_id: String,
+    data: JsonUpcase<GroupRequest>,
+    _headers: AdminHeaders,
+    conn: DbConn,
+) -> JsonResult {
+    put_group(_org_id, group_id, data, _headers, conn).await
+}
+
+#[post("/organizations/<org_id>/groups", data = "<data>")]
+async fn post_groups(
+    org_id: String,
+    _headers: AdminHeaders,
+    data: JsonUpcase<GroupRequest>,
+    conn: DbConn,
+) -> JsonResult {
+    let group_request = data.into_inner().data;
+    let group = group_request.to_group(&org_id)?;
+
+    add_update_group(group, group_request.Collections, &conn).await
+}
+
+#[put("/organizations/<_org_id>/groups/<group_id>", data = "<data>")]
+async fn put_group(
+    _org_id: String,
+    group_id: String,
+    data: JsonUpcase<GroupRequest>,
+    _headers: AdminHeaders,
+    conn: DbConn,
+) -> JsonResult {
+    let group = match Group::find_by_uuid(&group_id, &conn).await {
+        Some(group) => group,
+        None => err!("Group not found"),
+    };
+
+    let group_request = data.into_inner().data;
+    let updated_group = group_request.update_group(group)?;
+
+    CollectionGroup::delete_all_by_group(&group_id, &conn).await?;
+
+    add_update_group(updated_group, group_request.Collections, &conn).await
+}
+
+async fn add_update_group(mut group: Group, collections: Vec<SelectionReadOnly>, conn: &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?;
+    }
+
+    Ok(Json(json!({
+        "Id": group.uuid,
+        "OrganizationId": group.organizations_uuid,
+        "Name": group.name,
+        "AccessAll": group.access_all,
+        "ExternalId": group.get_external_id()
+    })))
+}
+
+#[get("/organizations/<_org_id>/groups/<group_id>/details")]
+async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
+    let group = match Group::find_by_uuid(&group_id, &conn).await {
+        Some(group) => group,
+        _ => err!("Group could not be found!"),
+    };
+
+    let collections_groups = CollectionGroup::find_by_group(&group_id, &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
+    })))
+}
+
+#[post("/organizations/<org_id>/groups/<group_id>/delete")]
+async fn post_delete_group(org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
+    delete_group(org_id, group_id, _headers, conn).await
+}
+
+#[delete("/organizations/<_org_id>/groups/<group_id>")]
+async fn delete_group(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
+    let group = match Group::find_by_uuid(&group_id, &conn).await {
+        Some(group) => group,
+        _ => err!("Group not found"),
+    };
+
+    group.delete(&conn).await
+}
+
+#[get("/organizations/<_org_id>/groups/<group_id>")]
+async fn get_group(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
+    let group = match Group::find_by_uuid(&group_id, &conn).await {
+        Some(group) => group,
+        _ => err!("Group not found"),
+    };
+
+    Ok(Json(group.to_json()))
+}
+
+#[get("/organizations/<_org_id>/groups/<group_id>/users")]
+async fn get_group_users(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
+    match Group::find_by_uuid(&group_id, &conn).await {
+        Some(_) => { /* Do nothing */ }
+        _ => err!("Group could not be found!"),
+    };
+
+    let group_users: Vec<String> = GroupUser::find_by_group(&group_id, &conn)
+        .await
+        .iter()
+        .map(|entry| entry.users_organizations_uuid.clone())
+        .collect();
+
+    Ok(Json(json!(group_users)))
+}
+
+#[put("/organizations/<_org_id>/groups/<group_id>/users", data = "<data>")]
+async fn put_group_users(
+    _org_id: String,
+    group_id: String,
+    _headers: AdminHeaders,
+    data: JsonVec<String>,
+    conn: DbConn,
+) -> EmptyResult {
+    match Group::find_by_uuid(&group_id, &conn).await {
+        Some(_) => { /* Do nothing */ }
+        _ => err!("Group could not be found!"),
+    };
+
+    GroupUser::delete_all_by_group(&group_id, &conn).await?;
+
+    let assigned_user_ids = data.into_inner();
+    for assigned_user_id in assigned_user_ids {
+        let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id);
+        user_entry.save(&conn).await?;
+    }
+
+    Ok(())
+}
+
+#[get("/organizations/<_org_id>/users/<user_id>/groups")]
+async fn get_user_groups(_org_id: String, user_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
+    match UserOrganization::find_by_uuid(&user_id, &conn).await {
+        Some(_) => { /* Do nothing */ }
+        _ => err!("User could not be found!"),
+    };
+
+    let user_groups: Vec<String> =
+        GroupUser::find_by_user(&user_id, &conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect();
+
+    Ok(Json(json!(user_groups)))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct OrganizationUserUpdateGroupsRequest {
+    GroupIds: Vec<String>,
+}
+
+#[post("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")]
+async fn post_user_groups(
+    _org_id: String,
+    user_id: String,
+    data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
+    _headers: AdminHeaders,
+    conn: DbConn,
+) -> EmptyResult {
+    put_user_groups(_org_id, user_id, data, _headers, conn).await
+}
+
+#[put("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")]
+async fn put_user_groups(
+    _org_id: String,
+    user_id: String,
+    data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
+    _headers: AdminHeaders,
+    conn: DbConn,
+) -> EmptyResult {
+    match UserOrganization::find_by_uuid(&user_id, &conn).await {
+        Some(_) => { /* Do nothing */ }
+        _ => err!("User could not be found!"),
+    };
+
+    GroupUser::delete_all_by_user(&user_id, &conn).await?;
+
+    let assigned_group_ids = data.into_inner().data;
+    for assigned_group_id in assigned_group_ids.GroupIds {
+        let mut group_user = GroupUser::new(assigned_group_id.clone(), user_id.clone());
+        group_user.save(&conn).await?;
+    }
+
+    Ok(())
+}
+
+#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<user_id>")]
+async fn post_delete_group_user(
+    org_id: String,
+    group_id: String,
+    user_id: String,
+    headers: AdminHeaders,
+    conn: DbConn,
+) -> EmptyResult {
+    delete_group_user(org_id, group_id, user_id, headers, conn).await
+}
+
+#[delete("/organizations/<_org_id>/groups/<group_id>/users/<user_id>")]
+async fn delete_group_user(
+    _org_id: String,
+    group_id: String,
+    user_id: String,
+    _headers: AdminHeaders,
+    conn: DbConn,
+) -> EmptyResult {
+    match UserOrganization::find_by_uuid(&user_id, &conn).await {
+        Some(_) => { /* Do nothing */ }
+        _ => err!("User could not be found!"),
+    };
+
+    match Group::find_by_uuid(&group_id, &conn).await {
+        Some(_) => { /* Do nothing */ }
+        _ => err!("Group could not be found!"),
+    };
+
+    GroupUser::delete_by_group_id_and_user_id(&group_id, &user_id, &conn).await
+}
+
 // This is a new function active since the v2022.9.x clients.
 // It combines the previous two calls done before.
 // We call those two functions here and combine them our selfs.
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 7bff978b9c6c02ad2ec4c9864ee92c992a03e1d9..49283dd22b40e2ac349daeb41bd0659ca79f5e6a 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -33,6 +33,7 @@ pub type EmptyResult = ApiResult<()>;
 
 type JsonUpcase<T> = Json<util::UpCase<T>>;
 type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>;
+type JsonVec<T> = Json<Vec<T>>;
 
 // Common structs representing JSON data received
 #[derive(Deserialize)]
diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs
index 7464fdc15554b442178f490b43c7b8027b3ae27f..2e82fc3794695123f61ac5ce7d6d8988a38dca4d 100644
--- a/src/db/models/cipher.rs
+++ b/src/db/models/cipher.rs
@@ -2,7 +2,9 @@ use crate::CONFIG;
 use chrono::{Duration, NaiveDateTime, Utc};
 use serde_json::Value;
 
-use super::{Attachment, CollectionCipher, Favorite, FolderCipher, User, UserOrgStatus, UserOrgType, UserOrganization};
+use super::{
+    Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
+};
 
 use crate::api::core::CipherSyncData;
 
@@ -337,7 +339,7 @@ impl Cipher {
     }
 
     /// Returns whether this cipher is owned by an org in which the user has full access.
-    pub async fn is_in_full_access_org(
+    async fn is_in_full_access_org(
         &self,
         user_uuid: &str,
         cipher_sync_data: Option<&CipherSyncData>,
@@ -355,6 +357,23 @@ impl Cipher {
         false
     }
 
+    /// Returns whether this cipher is owned by an group in which the user has full access.
+    async fn is_in_full_access_group(
+        &self,
+        user_uuid: &str,
+        cipher_sync_data: Option<&CipherSyncData>,
+        conn: &DbConn,
+    ) -> bool {
+        if let Some(ref org_uuid) = self.organization_uuid {
+            if let Some(cipher_sync_data) = cipher_sync_data {
+                return cipher_sync_data.user_group_full_access_for_organizations.get(org_uuid).is_some();
+            } else {
+                return Group::is_in_full_access_group(user_uuid, org_uuid, conn).await;
+            }
+        }
+        false
+    }
+
     /// 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
@@ -369,7 +388,10 @@ impl Cipher {
         // 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) || self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await {
+        if self.is_owned_by_user(user_uuid)
+            || self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await
+            || self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await
+        {
             return Some((false, false));
         }
 
@@ -377,14 +399,22 @@ impl Cipher {
             let mut rows: Vec<(bool, bool)> = Vec::new();
             if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
                 for collection in collections {
+                    //User permissions
                     if let Some(uc) = cipher_sync_data.user_collections.get(collection) {
                         rows.push((uc.read_only, uc.hide_passwords));
                     }
+
+                    //Group permissions
+                    if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
+                        rows.push((cg.read_only, cg.hide_passwords));
+                    }
                 }
             }
             rows
         } else {
-            self.get_collections_access_flags(user_uuid, conn).await
+            let mut access_flags = self.get_user_collections_access_flags(user_uuid, conn).await;
+            access_flags.append(&mut self.get_group_collections_access_flags(user_uuid, conn).await);
+            access_flags
         };
 
         if rows.is_empty() {
@@ -411,7 +441,7 @@ impl Cipher {
         Some((read_only, hide_passwords))
     }
 
-    pub async fn get_collections_access_flags(&self, user_uuid: &str, conn: &DbConn) -> Vec<(bool, bool)> {
+    async fn get_user_collections_access_flags(&self, user_uuid: &str, conn: &DbConn) -> Vec<(bool, bool)> {
         db_run! {conn: {
             // Check whether this cipher is in any collections accessible to the
             // user. If so, retrieve the access flags for each collection.
@@ -424,7 +454,30 @@ impl Cipher {
                         .and(users_collections::user_uuid.eq(user_uuid))))
                 .select((users_collections::read_only, users_collections::hide_passwords))
                 .load::<(bool, bool)>(conn)
-                .expect("Error getting access restrictions")
+                .expect("Error getting user access restrictions")
+        }}
+    }
+
+    async fn get_group_collections_access_flags(&self, user_uuid: &str, conn: &DbConn) -> Vec<(bool, bool)> {
+        db_run! {conn: {
+            ciphers::table
+                .filter(ciphers::uuid.eq(&self.uuid))
+                .inner_join(ciphers_collections::table.on(
+                    ciphers::uuid.eq(ciphers_collections::cipher_uuid)
+                ))
+                .inner_join(collections_groups::table.on(
+                    collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
+                ))
+                .inner_join(groups_users::table.on(
+                    groups_users::groups_uuid.eq(collections_groups::groups_uuid)
+                ))
+                .inner_join(users_organizations::table.on(
+                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)
+                ))
+                .filter(users_organizations::user_uuid.eq(user_uuid))
+                .select((collections_groups::read_only, collections_groups::hide_passwords))
+                .load::<(bool, bool)>(conn)
+                .expect("Error getting group access restrictions")
         }}
     }
 
@@ -477,10 +530,10 @@ impl Cipher {
     // Find all ciphers accessible or visible to the specified user.
     //
     // "Accessible" means the user has read access to the cipher, either via
-    // direct ownership or via collection access.
+    // direct ownership, collection or via group access.
     //
     // "Visible" usually means the same as accessible, except when an org
-    // owner/admin sets their account to have access to only selected
+    // owner/admin sets their account or group to have access to only selected
     // collections in the org (presumably because they aren't interested in
     // the other collections in the org). In this case, if `visible_only` is
     // true, then the non-interesting ciphers will not be returned. As a
@@ -502,9 +555,22 @@ impl Cipher {
                         // Ensure that users_collections::user_uuid is NULL for unconfirmed users.
                         .and(users_organizations::user_uuid.eq(users_collections::user_uuid))
                 ))
+                .left_join(groups_users::table.on(
+                    groups_users::users_organizations_uuid.eq(users_organizations::uuid)
+                ))
+                .left_join(groups::table.on(
+                    groups::uuid.eq(groups_users::groups_uuid)
+                ))
+                .left_join(collections_groups::table.on(
+                    collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(
+                        collections_groups::groups_uuid.eq(groups::uuid)
+                    )
+                ))
                 .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner
                 .or_filter(users_organizations::access_all.eq(true)) // access_all in org
                 .or_filter(users_collections::user_uuid.eq(user_uuid)) // Access to collection
+                .or_filter(groups::access_all.eq(true)) // Access via groups
+                .or_filter(collections_groups::collections_uuid.is_not_null()) // Access via groups
                 .into_boxed();
 
             if !visible_only {
@@ -630,11 +696,22 @@ impl Cipher {
                     users_collections::user_uuid.eq(user_id)
                 )
             ))
-            .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::atype.le(UserOrgType::Admin as i32) // User is admin or owner
+            .left_join(groups_users::table.on(
+                groups_users::users_organizations_uuid.eq(users_organizations::uuid)
+            ))
+            .left_join(groups::table.on(
+                groups::uuid.eq(groups_users::groups_uuid)
+            ))
+            .left_join(collections_groups::table.on(
+                collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(
+                    collections_groups::groups_uuid.eq(groups::uuid)
                 )
             ))
+            .or_filter(users_collections::user_uuid.eq(user_id)) // User has access to collection
+            .or_filter(users_organizations::access_all.eq(true)) // User has access all
+            .or_filter(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
+            .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)
             .load::<(String, String)>(conn).unwrap_or_default()
         }}
diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs
index 5d9464fd213ed7a08dfd7d2e41d1482dc4552e47..b7c1443427b4ac0665a14c408cd5002ec1ab3c2b 100644
--- a/src/db/models/collection.rs
+++ b/src/db/models/collection.rs
@@ -1,6 +1,6 @@
 use serde_json::Value;
 
-use super::{User, UserOrgStatus, UserOrgType, UserOrganization};
+use super::{CollectionGroup, User, UserOrgStatus, UserOrgType, UserOrganization};
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -127,6 +127,7 @@ impl Collection {
         self.update_users_revision(conn).await;
         CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?;
         CollectionUser::delete_all_by_collection(&self.uuid, conn).await?;
+        CollectionGroup::delete_all_by_collection(&self.uuid, conn).await?;
 
         db_run! { conn: {
             diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
@@ -171,14 +172,33 @@ impl Collection {
                     users_organizations::user_uuid.eq(user_uuid)
                 )
             ))
+            .left_join(groups_users::table.on(
+                groups_users::users_organizations_uuid.eq(users_organizations::uuid)
+            ))
+            .left_join(groups::table.on(
+                groups::uuid.eq(groups_users::groups_uuid)
+            ))
+            .left_join(collections_groups::table.on(
+                collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
+                    collections_groups::collections_uuid.eq(collections::uuid)
+                )
+            ))
             .filter(
                 users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
             )
             .filter(
                 users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
                     users_organizations::access_all.eq(true) // access_all in Organization
+                ).or(
+                    groups::access_all.eq(true) // access_all in groups
+                ).or( // access via groups
+                    groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(
+                        collections_groups::collections_uuid.is_not_null()
+                    )
                 )
-            ).select(collections::all_columns)
+            )
+            .select(collections::all_columns)
+            .distinct()
             .load::<CollectionDb>(conn).expect("Error loading collections").from_db()
         }}
     }
diff --git a/src/db/models/group.rs b/src/db/models/group.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eea4bcd2b7eb2ca9c81418efbea9801f418a6a80
--- /dev/null
+++ b/src/db/models/group.rs
@@ -0,0 +1,501 @@
+use chrono::{NaiveDateTime, Utc};
+use serde_json::Value;
+
+db_object! {
+    #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
+    #[table_name = "groups"]
+    #[primary_key(uuid)]
+    pub struct Group {
+        pub uuid: String,
+        pub organizations_uuid: String,
+        pub name: String,
+        pub access_all: bool,
+        external_id: Option<String>,
+        pub creation_date: NaiveDateTime,
+        pub revision_date: NaiveDateTime,
+    }
+
+    #[derive(Identifiable, Queryable, Insertable)]
+    #[table_name = "collections_groups"]
+    #[primary_key(collections_uuid, groups_uuid)]
+    pub struct CollectionGroup {
+        pub collections_uuid: String,
+        pub groups_uuid: String,
+        pub read_only: bool,
+        pub hide_passwords: bool,
+    }
+
+    #[derive(Identifiable, Queryable, Insertable)]
+    #[table_name = "groups_users"]
+    #[primary_key(groups_uuid, users_organizations_uuid)]
+    pub struct GroupUser {
+        pub groups_uuid: String,
+        pub users_organizations_uuid: String
+    }
+}
+
+/// Local methods
+impl Group {
+    pub fn new(organizations_uuid: String, name: String, access_all: bool, external_id: Option<String>) -> Self {
+        let now = Utc::now().naive_utc();
+
+        let mut new_model = Self {
+            uuid: crate::util::get_uuid(),
+            organizations_uuid,
+            name,
+            access_all,
+            external_id: None,
+            creation_date: now,
+            revision_date: now,
+        };
+
+        new_model.set_external_id(external_id);
+
+        new_model
+    }
+
+    pub fn to_json(&self) -> Value {
+        use crate::util::format_date;
+
+        json!({
+            "Id": self.uuid,
+            "OrganizationId": self.organizations_uuid,
+            "Name": self.name,
+            "AccessAll": self.access_all,
+            "ExternalId": self.external_id,
+            "CreationDate": format_date(&self.creation_date),
+            "RevisionDate": format_date(&self.revision_date)
+        })
+    }
+
+    pub fn set_external_id(&mut self, external_id: Option<String>) {
+        //Check if external id is empty. We don't want to have
+        //empty strings in the database
+        match external_id {
+            Some(external_id) => {
+                if external_id.is_empty() {
+                    self.external_id = None;
+                } else {
+                    self.external_id = Some(external_id)
+                }
+            }
+            None => self.external_id = None,
+        }
+    }
+
+    pub fn get_external_id(&self) -> Option<String> {
+        self.external_id.clone()
+    }
+}
+
+impl CollectionGroup {
+    pub fn new(collections_uuid: String, groups_uuid: String, read_only: bool, hide_passwords: bool) -> Self {
+        Self {
+            collections_uuid,
+            groups_uuid,
+            read_only,
+            hide_passwords,
+        }
+    }
+}
+
+impl GroupUser {
+    pub fn new(groups_uuid: String, users_organizations_uuid: String) -> Self {
+        Self {
+            groups_uuid,
+            users_organizations_uuid,
+        }
+    }
+}
+
+use crate::db::DbConn;
+
+use crate::api::EmptyResult;
+use crate::error::MapResult;
+
+use super::{User, UserOrganization};
+
+/// Database methods
+impl Group {
+    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        self.revision_date = Utc::now().naive_utc();
+
+        db_run! { conn:
+            sqlite, mysql {
+                match diesel::replace_into(groups::table)
+                    .values(GroupDb::to_db(self))
+                    .execute(conn)
+                {
+                    Ok(_) => Ok(()),
+                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
+                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
+                        diesel::update(groups::table)
+                            .filter(groups::uuid.eq(&self.uuid))
+                            .set(GroupDb::to_db(self))
+                            .execute(conn)
+                            .map_res("Error saving group")
+                    }
+                    Err(e) => Err(e.into()),
+                }.map_res("Error saving group")
+            }
+            postgresql {
+                let value = GroupDb::to_db(self);
+                diesel::insert_into(groups::table)
+                    .values(&value)
+                    .on_conflict(groups::uuid)
+                    .do_update()
+                    .set(&value)
+                    .execute(conn)
+                    .map_res("Error saving group")
+            }
+        }
+    }
+
+    pub async fn find_by_organization(organizations_uuid: &str, conn: &DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            groups::table
+                .filter(groups::organizations_uuid.eq(organizations_uuid))
+                .load::<GroupDb>(conn)
+                .expect("Error loading groups")
+                .from_db()
+        }}
+    }
+
+    pub async fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
+        db_run! { conn: {
+            groups::table
+                .filter(groups::uuid.eq(uuid))
+                .first::<GroupDb>(conn)
+                .ok()
+                .from_db()
+        }}
+    }
+
+    //Returns all organizations the user has full access to
+    pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &DbConn) -> Vec<String> {
+        db_run! { conn: {
+            groups_users::table
+                .inner_join(users_organizations::table.on(
+                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)
+                ))
+                .inner_join(groups::table.on(
+                    groups::uuid.eq(groups_users::groups_uuid)
+                ))
+                .filter(users_organizations::user_uuid.eq(user_uuid))
+                .filter(groups::access_all.eq(true))
+                .select(groups::organizations_uuid)
+                .distinct()
+                .load::<String>(conn)
+                .expect("Error loading organization group full access information for user")
+        }}
+    }
+
+    pub async fn is_in_full_access_group(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> bool {
+        db_run! { conn: {
+            groups::table
+                .inner_join(groups_users::table.on(
+                    groups_users::groups_uuid.eq(groups::uuid)
+                ))
+                .inner_join(users_organizations::table.on(
+                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)
+                ))
+                .filter(users_organizations::user_uuid.eq(user_uuid))
+                .filter(groups::organizations_uuid.eq(org_uuid))
+                .filter(groups::access_all.eq(true))
+                .select(groups::access_all)
+                .first::<bool>(conn)
+                .unwrap_or_default()
+        }}
+    }
+
+    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
+        CollectionGroup::delete_all_by_group(&self.uuid, conn).await?;
+        GroupUser::delete_all_by_group(&self.uuid, conn).await?;
+
+        db_run! { conn: {
+            diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid)))
+                .execute(conn)
+                .map_res("Error deleting group")
+        }}
+    }
+
+    pub async fn update_revision(uuid: &str, conn: &DbConn) {
+        if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
+            warn!("Failed to update revision for {}: {:#?}", uuid, e);
+        }
+    }
+
+    async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
+        db_run! {conn: {
+            crate::util::retry(|| {
+                diesel::update(groups::table.filter(groups::uuid.eq(uuid)))
+                    .set(groups::revision_date.eq(date))
+                    .execute(conn)
+            }, 10)
+            .map_res("Error updating group revision")
+        }}
+    }
+}
+
+impl CollectionGroup {
+    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;
+        for group_user in group_users {
+            group_user.update_user_revision(conn).await;
+        }
+
+        db_run! { conn:
+            sqlite, mysql {
+                match diesel::replace_into(collections_groups::table)
+                    .values((
+                        collections_groups::collections_uuid.eq(&self.collections_uuid),
+                        collections_groups::groups_uuid.eq(&self.groups_uuid),
+                        collections_groups::read_only.eq(&self.read_only),
+                        collections_groups::hide_passwords.eq(&self.hide_passwords),
+                    ))
+                    .execute(conn)
+                {
+                    Ok(_) => Ok(()),
+                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
+                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
+                        diesel::update(collections_groups::table)
+                            .filter(collections_groups::collections_uuid.eq(&self.collections_uuid))
+                            .filter(collections_groups::groups_uuid.eq(&self.groups_uuid))
+                            .set((
+                                collections_groups::collections_uuid.eq(&self.collections_uuid),
+                                collections_groups::groups_uuid.eq(&self.groups_uuid),
+                                collections_groups::read_only.eq(&self.read_only),
+                                collections_groups::hide_passwords.eq(&self.hide_passwords),
+                            ))
+                            .execute(conn)
+                            .map_res("Error adding group to collection")
+                    }
+                    Err(e) => Err(e.into()),
+                }.map_res("Error adding group to collection")
+            }
+            postgresql {
+                diesel::insert_into(collections_groups::table)
+                    .values((
+                        collections_groups::collections_uuid.eq(&self.collections_uuid),
+                        collections_groups::groups_uuid.eq(&self.groups_uuid),
+                        collections_groups::read_only.eq(self.read_only),
+                        collections_groups::hide_passwords.eq(self.hide_passwords),
+                    ))
+                    .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid))
+                    .do_update()
+                    .set((
+                        collections_groups::read_only.eq(self.read_only),
+                        collections_groups::hide_passwords.eq(self.hide_passwords),
+                    ))
+                    .execute(conn)
+                    .map_res("Error adding group to collection")
+            }
+        }
+    }
+
+    pub async fn find_by_group(group_uuid: &str, conn: &DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            collections_groups::table
+                .filter(collections_groups::groups_uuid.eq(group_uuid))
+                .load::<CollectionGroupDb>(conn)
+                .expect("Error loading collection groups")
+                .from_db()
+        }}
+    }
+
+    pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            collections_groups::table
+                .inner_join(groups_users::table.on(
+                    groups_users::groups_uuid.eq(collections_groups::groups_uuid)
+                ))
+                .inner_join(users_organizations::table.on(
+                    users_organizations::uuid.eq(groups_users::users_organizations_uuid)
+                ))
+                .filter(users_organizations::user_uuid.eq(user_uuid))
+                .select(collections_groups::all_columns)
+                .load::<CollectionGroupDb>(conn)
+                .expect("Error loading user collection groups")
+                .from_db()
+        }}
+    }
+
+    pub async fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            collections_groups::table
+                .filter(collections_groups::collections_uuid.eq(collection_uuid))
+                .select(collections_groups::all_columns)
+                .load::<CollectionGroupDb>(conn)
+                .expect("Error loading collection groups")
+                .from_db()
+        }}
+    }
+
+    pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
+        let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await;
+        for group_user in group_users {
+            group_user.update_user_revision(conn).await;
+        }
+
+        db_run! { conn: {
+            diesel::delete(collections_groups::table)
+                .filter(collections_groups::collections_uuid.eq(&self.collections_uuid))
+                .filter(collections_groups::groups_uuid.eq(&self.groups_uuid))
+                .execute(conn)
+                .map_res("Error deleting collection group")
+        }}
+    }
+
+    pub async fn delete_all_by_group(group_uuid: &str, conn: &DbConn) -> EmptyResult {
+        let group_users = GroupUser::find_by_group(group_uuid, conn).await;
+        for group_user in group_users {
+            group_user.update_user_revision(conn).await;
+        }
+
+        db_run! { conn: {
+            diesel::delete(collections_groups::table)
+                .filter(collections_groups::groups_uuid.eq(group_uuid))
+                .execute(conn)
+                .map_res("Error deleting collection group")
+        }}
+    }
+
+    pub async fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
+        let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
+        for collection_assigned_to_group in collection_assigned_to_groups {
+            let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await;
+            for group_user in group_users {
+                group_user.update_user_revision(conn).await;
+            }
+        }
+
+        db_run! { conn: {
+            diesel::delete(collections_groups::table)
+                .filter(collections_groups::collections_uuid.eq(collection_uuid))
+                .execute(conn)
+                .map_res("Error deleting collection group")
+        }}
+    }
+}
+
+impl GroupUser {
+    pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        self.update_user_revision(conn).await;
+
+        db_run! { conn:
+            sqlite, mysql {
+                match diesel::replace_into(groups_users::table)
+                    .values((
+                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
+                        groups_users::groups_uuid.eq(&self.groups_uuid),
+                    ))
+                    .execute(conn)
+                {
+                    Ok(_) => Ok(()),
+                    // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
+                    Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
+                        diesel::update(groups_users::table)
+                            .filter(groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid))
+                            .filter(groups_users::groups_uuid.eq(&self.groups_uuid))
+                            .set((
+                                groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
+                                groups_users::groups_uuid.eq(&self.groups_uuid),
+                            ))
+                            .execute(conn)
+                            .map_res("Error adding user to group")
+                    }
+                    Err(e) => Err(e.into()),
+                }.map_res("Error adding user to group")
+            }
+            postgresql {
+                diesel::insert_into(groups_users::table)
+                    .values((
+                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
+                        groups_users::groups_uuid.eq(&self.groups_uuid),
+                    ))
+                    .on_conflict((groups_users::users_organizations_uuid, groups_users::groups_uuid))
+                    .do_update()
+                    .set((
+                        groups_users::users_organizations_uuid.eq(&self.users_organizations_uuid),
+                        groups_users::groups_uuid.eq(&self.groups_uuid),
+                    ))
+                    .execute(conn)
+                    .map_res("Error adding user to group")
+            }
+        }
+    }
+
+    pub async fn find_by_group(group_uuid: &str, conn: &DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            groups_users::table
+                .filter(groups_users::groups_uuid.eq(group_uuid))
+                .load::<GroupUserDb>(conn)
+                .expect("Error loading group users")
+                .from_db()
+        }}
+    }
+
+    pub async fn find_by_user(users_organizations_uuid: &str, conn: &DbConn) -> Vec<Self> {
+        db_run! { conn: {
+            groups_users::table
+                .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
+                .load::<GroupUserDb>(conn)
+                .expect("Error loading groups for user")
+                .from_db()
+        }}
+    }
+
+    pub async fn update_user_revision(&self, conn: &DbConn) {
+        match UserOrganization::find_by_uuid(&self.users_organizations_uuid, conn).await {
+            Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
+            None => warn!("User could not be found!"),
+        }
+    }
+
+    pub async fn delete_by_group_id_and_user_id(
+        group_uuid: &str,
+        users_organizations_uuid: &str,
+        conn: &DbConn,
+    ) -> EmptyResult {
+        match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
+            Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
+            None => warn!("User could not be found!"),
+        };
+
+        db_run! { conn: {
+            diesel::delete(groups_users::table)
+                .filter(groups_users::groups_uuid.eq(group_uuid))
+                .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
+                .execute(conn)
+                .map_res("Error deleting group users")
+        }}
+    }
+
+    pub async fn delete_all_by_group(group_uuid: &str, conn: &DbConn) -> EmptyResult {
+        let group_users = GroupUser::find_by_group(group_uuid, conn).await;
+        for group_user in group_users {
+            group_user.update_user_revision(conn).await;
+        }
+
+        db_run! { conn: {
+            diesel::delete(groups_users::table)
+                .filter(groups_users::groups_uuid.eq(group_uuid))
+                .execute(conn)
+                .map_res("Error deleting group users")
+        }}
+    }
+
+    pub async fn delete_all_by_user(users_organizations_uuid: &str, conn: &DbConn) -> EmptyResult {
+        match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
+            Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
+            None => warn!("User could not be found!"),
+        }
+
+        db_run! { conn: {
+            diesel::delete(groups_users::table)
+                .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
+                .execute(conn)
+                .map_res("Error deleting user groups")
+        }}
+    }
+}
diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs
index eb425d1ac903b654600846c80ff9d1dfb6f4d332..20e659c64da0726b03025ae88923d5240204c2ef 100644
--- a/src/db/models/mod.rs
+++ b/src/db/models/mod.rs
@@ -5,6 +5,7 @@ mod device;
 mod emergency_access;
 mod favorite;
 mod folder;
+mod group;
 mod org_policy;
 mod organization;
 mod send;
@@ -19,6 +20,7 @@ pub use self::device::Device;
 pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
 pub use self::favorite::Favorite;
 pub use self::folder::{Folder, FolderCipher};
+pub use self::group::{CollectionGroup, Group, GroupUser};
 pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
 pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
 pub use self::send::{Send, SendType};
diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs
index 99787eb85754833559becbb401d68909befb72f7..fa9be4ec16a9f36ebff21a809b722d38731c9456 100644
--- a/src/db/models/organization.rs
+++ b/src/db/models/organization.rs
@@ -2,7 +2,7 @@ use num_traits::FromPrimitive;
 use serde_json::Value;
 use std::cmp::Ordering;
 
-use super::{CollectionUser, OrgPolicy, OrgPolicyType, User};
+use super::{CollectionUser, GroupUser, OrgPolicy, OrgPolicyType, User};
 
 db_object! {
     #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -148,7 +148,7 @@ impl Organization {
             "Use2fa": true,
             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
             "UseEvents": false, // Not supported
-            "UseGroups": false, // Not supported
+            "UseGroups": true,
             "UseTotp": true,
             "UsePolicies": true,
             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
@@ -300,7 +300,7 @@ impl UserOrganization {
             "Use2fa": true,
             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
             "UseEvents": false, // Not supported
-            "UseGroups": false, // Not supported
+            "UseGroups": true,
             "UseTotp": true,
             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
             "UsePolicies": true,
@@ -459,6 +459,7 @@ impl UserOrganization {
         User::update_uuid_revision(&self.user_uuid, conn).await;
 
         CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?;
+        GroupUser::delete_all_by_user(&self.uuid, conn).await?;
 
         db_run! { conn: {
             diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs
index a49159f27c33121023ae900de2c5ff5e80d6d561..514bc67a13ee389c81bcdfee69daec1cb0207521 100644
--- a/src/db/schemas/mysql/schema.rs
+++ b/src/db/schemas/mysql/schema.rs
@@ -220,6 +220,34 @@ table! {
     }
 }
 
+table! {
+    groups (uuid) {
+        uuid -> Text,
+        organizations_uuid -> Text,
+        name -> Text,
+        access_all -> Bool,
+        external_id -> Nullable<Text>,
+        creation_date -> Timestamp,
+        revision_date -> Timestamp,
+    }
+}
+
+table! {
+    groups_users (groups_uuid, users_organizations_uuid) {
+        groups_uuid -> Text,
+        users_organizations_uuid -> Text,
+    }
+}
+
+table! {
+    collections_groups (collections_uuid, groups_uuid) {
+        collections_uuid -> Text,
+        groups_uuid -> Text,
+        read_only -> Bool,
+        hide_passwords -> Bool,
+    }
+}
+
 joinable!(attachments -> ciphers (cipher_uuid));
 joinable!(ciphers -> organizations (organization_uuid));
 joinable!(ciphers -> users (user_uuid));
@@ -239,6 +267,11 @@ joinable!(users_collections -> users (user_uuid));
 joinable!(users_organizations -> organizations (org_uuid));
 joinable!(users_organizations -> users (user_uuid));
 joinable!(emergency_access -> users (grantor_uuid));
+joinable!(groups -> organizations (organizations_uuid));
+joinable!(groups_users -> users_organizations (users_organizations_uuid));
+joinable!(groups_users -> groups (groups_uuid));
+joinable!(collections_groups -> collections (collections_uuid));
+joinable!(collections_groups -> groups (groups_uuid));
 
 allow_tables_to_appear_in_same_query!(
     attachments,
@@ -257,4 +290,7 @@ allow_tables_to_appear_in_same_query!(
     users_collections,
     users_organizations,
     emergency_access,
+    groups,
+    groups_users,
+    collections_groups,
 );
diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs
index 9fd6fd9727e01a5c578ee799eb8ef79b9b8e82cc..23f9af7ef3fb12abcc2842ea46465d31c1e96a75 100644
--- a/src/db/schemas/postgresql/schema.rs
+++ b/src/db/schemas/postgresql/schema.rs
@@ -220,6 +220,34 @@ table! {
     }
 }
 
+table! {
+    groups (uuid) {
+        uuid -> Text,
+        organizations_uuid -> Text,
+        name -> Text,
+        access_all -> Bool,
+        external_id -> Nullable<Text>,
+        creation_date -> Timestamp,
+        revision_date -> Timestamp,
+    }
+}
+
+table! {
+    groups_users (groups_uuid, users_organizations_uuid) {
+        groups_uuid -> Text,
+        users_organizations_uuid -> Text,
+    }
+}
+
+table! {
+    collections_groups (collections_uuid, groups_uuid) {
+        collections_uuid -> Text,
+        groups_uuid -> Text,
+        read_only -> Bool,
+        hide_passwords -> Bool,
+    }
+}
+
 joinable!(attachments -> ciphers (cipher_uuid));
 joinable!(ciphers -> organizations (organization_uuid));
 joinable!(ciphers -> users (user_uuid));
@@ -239,6 +267,11 @@ joinable!(users_collections -> users (user_uuid));
 joinable!(users_organizations -> organizations (org_uuid));
 joinable!(users_organizations -> users (user_uuid));
 joinable!(emergency_access -> users (grantor_uuid));
+joinable!(groups -> organizations (organizations_uuid));
+joinable!(groups_users -> users_organizations (users_organizations_uuid));
+joinable!(groups_users -> groups (groups_uuid));
+joinable!(collections_groups -> collections (collections_uuid));
+joinable!(collections_groups -> groups (groups_uuid));
 
 allow_tables_to_appear_in_same_query!(
     attachments,
@@ -257,4 +290,7 @@ allow_tables_to_appear_in_same_query!(
     users_collections,
     users_organizations,
     emergency_access,
+    groups,
+    groups_users,
+    collections_groups,
 );
diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs
index 9fd6fd9727e01a5c578ee799eb8ef79b9b8e82cc..23f9af7ef3fb12abcc2842ea46465d31c1e96a75 100644
--- a/src/db/schemas/sqlite/schema.rs
+++ b/src/db/schemas/sqlite/schema.rs
@@ -220,6 +220,34 @@ table! {
     }
 }
 
+table! {
+    groups (uuid) {
+        uuid -> Text,
+        organizations_uuid -> Text,
+        name -> Text,
+        access_all -> Bool,
+        external_id -> Nullable<Text>,
+        creation_date -> Timestamp,
+        revision_date -> Timestamp,
+    }
+}
+
+table! {
+    groups_users (groups_uuid, users_organizations_uuid) {
+        groups_uuid -> Text,
+        users_organizations_uuid -> Text,
+    }
+}
+
+table! {
+    collections_groups (collections_uuid, groups_uuid) {
+        collections_uuid -> Text,
+        groups_uuid -> Text,
+        read_only -> Bool,
+        hide_passwords -> Bool,
+    }
+}
+
 joinable!(attachments -> ciphers (cipher_uuid));
 joinable!(ciphers -> organizations (organization_uuid));
 joinable!(ciphers -> users (user_uuid));
@@ -239,6 +267,11 @@ joinable!(users_collections -> users (user_uuid));
 joinable!(users_organizations -> organizations (org_uuid));
 joinable!(users_organizations -> users (user_uuid));
 joinable!(emergency_access -> users (grantor_uuid));
+joinable!(groups -> organizations (organizations_uuid));
+joinable!(groups_users -> users_organizations (users_organizations_uuid));
+joinable!(groups_users -> groups (groups_uuid));
+joinable!(collections_groups -> collections (collections_uuid));
+joinable!(collections_groups -> groups (groups_uuid));
 
 allow_tables_to_appear_in_same_query!(
     attachments,
@@ -257,4 +290,7 @@ allow_tables_to_appear_in_same_query!(
     users_collections,
     users_organizations,
     emergency_access,
+    groups,
+    groups_users,
+    collections_groups,
 );