aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--migrations/mysql/2022-07-27-110000_add_group_support/down.sql3
-rw-r--r--migrations/mysql/2022-07-27-110000_add_group_support/up.sql23
-rw-r--r--migrations/postgresql/2022-07-27-110000_add_group_support/down.sql3
-rw-r--r--migrations/postgresql/2022-07-27-110000_add_group_support/up.sql23
-rw-r--r--migrations/sqlite/2022-07-27-110000_add_group_support/down.sql3
-rw-r--r--migrations/sqlite/2022-07-27-110000_add_group_support/up.sql23
-rw-r--r--src/api/core/ciphers.rs14
-rw-r--r--src/api/core/organizations.rs373
-rw-r--r--src/api/mod.rs1
-rw-r--r--src/db/models/cipher.rs99
-rw-r--r--src/db/models/collection.rs24
-rw-r--r--src/db/models/group.rs501
-rw-r--r--src/db/models/mod.rs2
-rw-r--r--src/db/models/organization.rs7
-rw-r--r--src/db/schemas/mysql/schema.rs36
-rw-r--r--src/db/schemas/postgresql/schema.rs36
-rw-r--r--src/db/schemas/sqlite/schema.rs36
17 files changed, 1188 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
new file mode 100644
index 00000000..0ec5c501
--- /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 00000000..6d40638a
--- /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 00000000..9a12d694
--- /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 00000000..5eed1df3
--- /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 00000000..9a12d694
--- /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 00000000..0523c760
--- /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 1870d12b..051d9476 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 9f2178e7..07baa7ba 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 7bff978b..49283dd2 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 7464fdc1..2e82fc37 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 5d9464fd..b7c14434 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 00000000..eea4bcd2
--- /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 eb425d1a..20e659c6 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 99787eb8..fa9be4ec 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 a49159f2..514bc67a 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 9fd6fd97..23f9af7e 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 9fd6fd97..23f9af7e 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,
);