diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index e968ffbd..20c951ed 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -6,7 +6,7 @@ use serde_json::Value; use crate::{ api::{ core::{CipherSyncData, CipherSyncType}, - EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType, + EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordData, UpdateType, }, auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{models::*, DbConn}, @@ -61,6 +61,21 @@ pub fn routes() -> Vec { import, post_org_keys, bulk_public_keys, + 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, ] } @@ -1484,3 +1499,282 @@ async fn import(org_id: String, data: JsonUpcase, headers: Header Ok(()) } + +#[get("/organizations//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::(); + + Ok(Json(json!({ + "Data": groups, + "Object": "list", + "ContinuationToken": null, + }))) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct GroupRequest { + Name: String, + AccessAll: Option, + ExternalId: String, + Collections: Vec +} + +impl GroupRequest { + pub fn to_group(&self, organizations_uuid: &str) -> Result { + let access_all_value = match self.AccessAll { + Some(value) => value, + _ => return Err(String::from("Could not convert GroupRequest to Group, because AccessAll has no value!")) + }; + + Ok(Group::new( + organizations_uuid.to_owned(), + self.Name.clone(), + access_all_value, + self.ExternalId.clone() + )) + } + + pub fn update_group(&self, mut group: Group) -> Result { + let access_all_value = match self.AccessAll { + Some(value) => value, + _ => return Err(String::from("Could not update group, because AccessAll has no value!")) + }; + + group.name = self.Name.clone(); + group.access_all = access_all_value; + group.external_id = self.ExternalId.clone(); + + Ok(group) + } +} + +#[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.clone(), + self.ReadOnly, + self.HidePasswords + ) + } + + pub fn to_selection_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_json (&self) -> Value { + json!(self) + } +} + +#[post("/organizations/<_org_id>/groups/", data = "")] +async fn post_group(_org_id: String, group_id: String, data: JsonUpcase, _headers: AdminHeaders, conn: DbConn) -> JsonResult { + put_group(_org_id, group_id, data, _headers, conn).await +} + +#[post("/organizations//groups", data = "")] +async fn post_groups(org_id: String, _headers: AdminHeaders, data: JsonUpcase, conn: DbConn) -> JsonResult { + let group_request = data.into_inner().data; + let group = match group_request.to_group(&org_id) { + Ok(group) => group, + Err(err) => err!(&err) + }; + + add_update_group(group, group_request.Collections, &conn).await +} + +#[put("/organizations/<_org_id>/groups/", data = "")] +async fn put_group(_org_id: String, group_id: String, data: JsonUpcase, _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 = match group_request.update_group(group) { + Ok(group) => group, + Err(err) => err!(&err) + }; + + 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, 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.external_id + }))) +} + +#[get("/organizations/<_org_id>/groups//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 collection_groups = CollectionGroup::find_by_group(&group_id, &conn).await + .iter() + .map(|entry| SelectionReadOnly::to_selection_read_only(entry).to_json()) + .collect::(); + + Ok(Json(json!({ + "Id": group.uuid, + "OrganizationId": group.organizations_uuid, + "Name": group.name, + "AccessAll": group.access_all, + "ExternalId": group.external_id, + "Collections": collection_groups + }))) +} + +#[post("/organizations//groups//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/")] +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/")] +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//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 = 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//users", data = "")] +async fn put_group_users(_org_id: String, group_id: String, _headers: AdminHeaders, data: JsonVec, 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//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 = 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 +} + +#[post("/organizations/<_org_id>/users//groups", data ="")] +async fn post_user_groups(_org_id: String, user_id: String, data: JsonUpcase, _headers: AdminHeaders, conn: DbConn) -> EmptyResult { + put_user_groups(_org_id, user_id, data, _headers, conn).await +} + +#[put("/organizations/<_org_id>/users//groups", data ="")] +async fn put_user_groups(_org_id: String, user_id: String, data: JsonUpcase, _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//groups//delete-user/")] +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//users/")] +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 +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index 99fb98be..3c9fb047 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -30,6 +30,7 @@ pub type EmptyResult = ApiResult<()>; type JsonUpcase = Json>; type JsonUpcaseVec = Json>>; +type JsonVec = Json>; // Common structs representing JSON data received #[derive(Deserialize)] diff --git a/src/db/models/group.rs b/src/db/models/group.rs new file mode 100644 index 00000000..e90832cf --- /dev/null +++ b/src/db/models/group.rs @@ -0,0 +1,355 @@ +use serde_json::Value; +use chrono::{NaiveDateTime, Utc}; + +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, + pub external_id: String, + pub creation_date: NaiveDateTime, + pub revision_date: NaiveDateTime, + } + + #[derive(Identifiable, Queryable, Insertable)] + #[table_name = "collection_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: String) -> Self { + let now = Utc::now().naive_utc(); + + Self { + uuid: crate::util::get_uuid(), + organizations_uuid: organizations_uuid, + name: name, + access_all: access_all, + external_id: external_id, + creation_date: now, + revision_date: now + } + } + + 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) + }) + } +} + +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: groups_uuid, + users_organizations_uuid: users_organizations_uuid + } + } +} + +use crate::db::DbConn; + +use crate::api::EmptyResult; +use crate::error::MapResult; + +/// 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 { + db_run! { conn: { + groups::table + .filter(groups::organizations_uuid.eq(organizations_uuid)) + .load::(conn) + .expect("Error loading groups") + .from_db() + }} + } + + pub async fn find_by_uuid (uuid: &str, conn: &DbConn) -> Option { + db_run! { conn: { + groups::table + .filter(groups::uuid.eq(uuid)) + .first::(conn) + .ok() + .from_db() + }} + } + + 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 { + Group::update_revision(&self.groups_uuid, &conn).await; + + db_run! { conn: + sqlite, mysql { + match diesel::replace_into(collection_groups::table) + .values(( + collection_groups::collections_uuid.eq(collections_uuid), + collection_groups::groups_uuid.eq(groups_uuid), + collection_groups::read_only.eq(read_only), + collection_groups::hide_passwords.eq(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(collection_groups::table) + .filter(collection_groups::collections_uuid.eq(self.collections_uuid)) + .filter(collection_groups::groups_uuid.eq(self.groups_uuid)) + .set(( + collection_groups::collections_uuid.eq(self.collections_uuid), + collection_groups::groups_uuid.eq(self.groups_uuid), + collection_groups::read_only.eq(self.read_only), + collection_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(collection_groups::table) + .values(( + collection_groups::collections_uuid.eq(&self.collections_uuid), + collection_groups::groups_uuid.eq(&self.groups_uuid), + collection_groups::read_only.eq(self.read_only), + collection_groups::hide_passwords.eq(self.hide_passwords), + )) + .on_conflict((collection_groups::collections_uuid, collection_groups::groups_uuid)) + .do_update() + .set(( + collection_groups::read_only.eq(self.read_only), + collection_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 { + db_run! { conn: { + collection_groups::table + .filter(collection_groups::groups_uuid.eq(group_uuid)) + .load::(conn) + .expect("Error loading collection groups") + .from_db() + }} + } + + pub async fn delete(&self, conn: &DbConn) -> EmptyResult { + db_run! { conn: { + diesel::delete(collection_groups::table) + .filter(collection_groups::collections_uuid.eq(&self.collections_uuid)) + .filter(collection_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 { + db_run! { conn: { + diesel::delete(collection_groups::table) + .filter(collection_groups::groups_uuid.eq(group_uuid)) + .execute(conn) + .map_res("Error deleting collection group") + }} + } +} + +impl GroupUser { + pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { + Group::update_revision(&self.groups_uuid, &conn).await; + + db_run! { conn: + sqlite, mysql { + match diesel::replace_into(groups_users::table) + .values(( + groups_users::users_organizations_uuid.eq(users_organizations_uuid), + groups_users::groups_uuid.eq(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 { + db_run! { conn: { + groups_users::table + .filter(groups_users::groups_uuid.eq(group_uuid)) + .load::(conn) + .expect("Error loading group users") + .from_db() + }} + } + + pub async fn find_by_user(users_organizations_uuid: &str, conn: &DbConn) -> Vec { + db_run! { conn: { + groups_users::table + .filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid)) + .load::(conn) + .expect("Error loading groups for user") + .from_db() + }} + } + + pub async fn delete_by_group_id_and_user_id(group_uuid: &str, user_uuid: &str, conn: &DbConn) -> EmptyResult { + db_run! { conn: { + diesel::delete(groups_users::table) + .filter(groups_users::groups_uuid.eq(group_uuid)) + .filter(groups_users::users_organizations_uuid.eq(user_uuid)) + .execute(conn) + .map_res("Error deleting group users") + }} + } + + pub async fn delete_all_by_group(group_uuid: &str, conn: &DbConn) -> EmptyResult { + 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 { + 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") + }} + } +} \ No newline at end of file diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 251511da..f64e3c6b 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -11,6 +11,7 @@ mod send; mod two_factor; mod two_factor_incomplete; mod user; +mod group; pub use self::attachment::Attachment; pub use self::cipher::Cipher; @@ -25,3 +26,4 @@ pub use self::send::{Send, SendType}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_incomplete::TwoFactorIncomplete; pub use self::user::{Invitation, User, UserStampException}; +pub use self::group::{Group, CollectionGroup, GroupUser}; \ No newline at end of file diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 362d3ffc..49b21b1c 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, OrgPolicy, OrgPolicyType, User, GroupUser}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -415,6 +415,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)))