mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2024-11-21 12:25:23 +03:00
group support
This commit is contained in:
parent
4cb5122e90
commit
21bc3bfd53
17 changed files with 1188 additions and 19 deletions
|
@ -0,0 +1,3 @@
|
|||
DROP TABLE `groups`;
|
||||
DROP TABLE groups_users;
|
||||
DROP TABLE collections_groups;
|
23
migrations/mysql/2022-07-27-110000_add_group_support/up.sql
Normal file
23
migrations/mysql/2022-07-27-110000_add_group_support/up.sql
Normal file
|
@ -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)
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
DROP TABLE groups;
|
||||
DROP TABLE groups_users;
|
||||
DROP TABLE collections_groups;
|
|
@ -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)
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
DROP TABLE groups;
|
||||
DROP TABLE groups_users;
|
||||
DROP TABLE collections_groups;
|
23
migrations/sqlite/2022-07-27-110000_add_group_support/up.sql
Normal file
23
migrations/sqlite/2022-07-27-110000_add_group_support/up.sql
Normal file
|
@ -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)
|
||||
);
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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()
|
||||
}}
|
||||
|
|
|
@ -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()
|
||||
}}
|
||||
}
|
||||
|
|
501
src/db/models/group.rs
Normal file
501
src/db/models/group.rs
Normal file
|
@ -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")
|
||||
}}
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue