commit 21bc3bfd538157b341191c9caeddc86ca6fe49c3 parent 4cb5122e908b57efa1bd1b3c0a8707f129cb4823 Author: MFijak <maxi_fijak@hotmail.de> Date: Thu, 20 Oct 2022 15:31:53 +0200 group support Diffstat:
17 files changed, 1194 insertions(+), 19 deletions(-)
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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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, );