group support

This commit is contained in:
MFijak
2022-10-20 15:31:53 +02:00
parent 4cb5122e90
commit 21bc3bfd53
17 changed files with 1188 additions and 19 deletions

View File

@@ -0,0 +1,3 @@
DROP TABLE `groups`;
DROP TABLE groups_users;
DROP TABLE collections_groups;

View 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)
);

View File

@@ -0,0 +1,3 @@
DROP TABLE groups;
DROP TABLE groups_users;
DROP TABLE collections_groups;

View 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 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)
);

View File

@@ -0,0 +1,3 @@
DROP TABLE groups;
DROP TABLE groups_users;
DROP TABLE collections_groups;

View 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)
);

View File

@@ -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,
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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)]

View File

@@ -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()
}}

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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};

View File

@@ -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)))

View File

@@ -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,
);

View File

@@ -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,
);

View File

@@ -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,
);