Misc org fixes (#7032)

* Split vault org/personal purge endpoints

Signed-off-by: BlackDex <black.dex@gmail.com>

* Adjust several other call-sites

Signed-off-by: BlackDex <black.dex@gmail.com>

* Several other misc fixes

Signed-off-by: BlackDex <black.dex@gmail.com>

* Add some more validation for groups, collections and memberships

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
Mathijs van Veluw
2026-03-29 23:15:48 +02:00
committed by GitHub
parent f62a7a66c8
commit 787822854c
12 changed files with 343 additions and 194 deletions

View File

@@ -106,7 +106,6 @@ pub struct RegisterData {
name: Option<String>, name: Option<String>,
#[allow(dead_code)]
organization_user_id: Option<MembershipId>, organization_user_id: Option<MembershipId>,
// Used only from the register/finish endpoint // Used only from the register/finish endpoint
@@ -376,14 +375,12 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
if let Some(identifier) = data.org_identifier { if let Some(identifier) = data.org_identifier {
if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID { if identifier != crate::sso::FAKE_IDENTIFIER && identifier != crate::api::admin::FAKE_ADMIN_UUID {
let org = match Organization::find_by_uuid(&identifier.into(), &conn).await { let Some(org) = Organization::find_by_uuid(&identifier.into(), &conn).await else {
None => err!("Failed to retrieve the associated organization"), err!("Failed to retrieve the associated organization")
Some(org) => org,
}; };
let membership = match Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await { let Some(membership) = Membership::find_by_user_and_org(&user.uuid, &org.uuid, &conn).await else {
None => err!("Failed to retrieve the invitation"), err!("Failed to retrieve the invitation")
Some(org) => org,
}; };
accept_org_invite(&user, membership, None, &conn).await?; accept_org_invite(&user, membership, None, &conn).await?;
@@ -583,7 +580,6 @@ fn set_kdf_data(user: &mut User, data: &KDFData) -> EmptyResult {
Ok(()) Ok(())
} }
#[allow(dead_code)]
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct AuthenticationData { struct AuthenticationData {
@@ -592,7 +588,6 @@ struct AuthenticationData {
master_password_authentication_hash: String, master_password_authentication_hash: String,
} }
#[allow(dead_code)]
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct UnlockData { struct UnlockData {
@@ -601,11 +596,12 @@ struct UnlockData {
master_key_wrapped_user_key: String, master_key_wrapped_user_key: String,
} }
#[allow(dead_code)]
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ChangeKdfData { struct ChangeKdfData {
#[allow(dead_code)]
new_master_password_hash: String, new_master_password_hash: String,
#[allow(dead_code)]
key: String, key: String,
authentication_data: AuthenticationData, authentication_data: AuthenticationData,
unlock_data: UnlockData, unlock_data: UnlockData,

View File

@@ -14,7 +14,7 @@ use crate::auth::ClientVersion;
use crate::util::{save_temp_file, NumberOrString}; use crate::util::{save_temp_file, NumberOrString};
use crate::{ use crate::{
api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType},
auth::Headers, auth::{Headers, OrgIdGuard, OwnerHeaders},
config::PathType, config::PathType,
crypto, crypto,
db::{ db::{
@@ -86,7 +86,8 @@ pub fn routes() -> Vec<Route> {
restore_cipher_put_admin, restore_cipher_put_admin,
restore_cipher_selected, restore_cipher_selected,
restore_cipher_selected_admin, restore_cipher_selected_admin,
delete_all, purge_org_vault,
purge_personal_vault,
move_cipher_selected, move_cipher_selected,
move_cipher_selected_put, move_cipher_selected_put,
put_collections2_update, put_collections2_update,
@@ -425,7 +426,7 @@ pub async fn update_cipher_from_data(
let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some(); let transfer_cipher = cipher.organization_uuid.is_none() && data.organization_id.is_some();
if let Some(org_id) = data.organization_id { if let Some(org_id) = data.organization_id {
match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { match Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, conn).await {
None => err!("You don't have permission to add item to organization"), None => err!("You don't have permission to add item to organization"),
Some(member) => { Some(member) => {
if shared_to_collections.is_some() if shared_to_collections.is_some()
@@ -1642,9 +1643,51 @@ struct OrganizationIdData {
org_id: OrganizationId, org_id: OrganizationId,
} }
// Use the OrgIdGuard here, to ensure there an organization id present.
// If there is no organization id present, it should be forwarded to purge_personal_vault.
// This guard needs to be the first argument, else OwnerHeaders will be triggered which will logout the user.
#[post("/ciphers/purge?<organization..>", data = "<data>")] #[post("/ciphers/purge?<organization..>", data = "<data>")]
async fn delete_all( async fn purge_org_vault(
organization: Option<OrganizationIdData>, _org_id_guard: OrgIdGuard,
organization: OrganizationIdData,
data: Json<PasswordOrOtpData>,
headers: OwnerHeaders,
conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
if organization.org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
data.validate(&user, true, &conn).await?;
match Membership::find_confirmed_by_user_and_org(&user.uuid, &organization.org_id, &conn).await {
Some(member) if member.atype == MembershipType::Owner => {
Cipher::delete_all_by_organization(&organization.org_id, &conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
log_event(
EventType::OrganizationPurgedVault as i32,
&organization.org_id,
&organization.org_id,
&user.uuid,
headers.device.atype,
&headers.ip.ip,
&conn,
)
.await;
Ok(())
}
_ => err!("You don't have permission to purge the organization vault"),
}
}
#[post("/ciphers/purge", data = "<data>")]
async fn purge_personal_vault(
data: Json<PasswordOrOtpData>, data: Json<PasswordOrOtpData>,
headers: Headers, headers: Headers,
conn: DbConn, conn: DbConn,
@@ -1655,42 +1698,10 @@ async fn delete_all(
data.validate(&user, true, &conn).await?; data.validate(&user, true, &conn).await?;
match organization {
Some(org_data) => {
// Organization ID in query params, purging organization vault
match Membership::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn).await {
None => err!("You don't have permission to purge the organization vault"),
Some(member) => {
if member.atype == MembershipType::Owner {
Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
log_event(
EventType::OrganizationPurgedVault as i32,
&org_data.org_id,
&org_data.org_id,
&user.uuid,
headers.device.atype,
&headers.ip.ip,
&conn,
)
.await;
Ok(())
} else {
err!("You don't have permission to purge the organization vault");
}
}
}
}
None => {
// No organization ID in query params, purging user vault
// Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await { for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await {
cipher.delete(&conn).await?; cipher.delete(&conn).await?;
} }
// Delete folders
for f in Folder::find_by_user(&user.uuid, &conn).await { for f in Folder::find_by_user(&user.uuid, &conn).await {
f.delete(&conn).await?; f.delete(&conn).await?;
} }
@@ -1699,8 +1710,6 @@ async fn delete_all(
nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await; nt.send_user_update(UpdateType::SyncVault, &user, &headers.device.push_uuid, &conn).await;
Ok(()) Ok(())
}
}
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@@ -1980,8 +1989,11 @@ impl CipherSyncData {
} }
// Generate a HashMap with the Organization UUID as key and the Membership record // Generate a HashMap with the Organization UUID as key and the Membership record
let members: HashMap<OrganizationId, Membership> = let members: HashMap<OrganizationId, Membership> = Membership::find_confirmed_by_user(user_id, conn)
Membership::find_by_user(user_id, conn).await.into_iter().map(|m| (m.org_uuid.clone(), m)).collect(); .await
.into_iter()
.map(|m| (m.org_uuid.clone(), m))
.collect();
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn) let user_collections: HashMap<CollectionId, CollectionUser> = CollectionUser::find_by_user(user_id, conn)

View File

@@ -240,7 +240,7 @@ async fn _log_user_event(
ip: &IpAddr, ip: &IpAddr,
conn: &DbConn, conn: &DbConn,
) { ) {
let memberships = Membership::find_by_user(user_id, conn).await; let memberships = Membership::find_confirmed_by_user(user_id, conn).await;
let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org let mut events: Vec<Event> = Vec::with_capacity(memberships.len() + 1); // We need an event per org and one without an org
// Upstream saves the event also without any org_id. // Upstream saves the event also without any org_id.

View File

@@ -131,6 +131,24 @@ struct FullCollectionData {
external_id: Option<String>, external_id: Option<String>,
} }
impl FullCollectionData {
pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
let org_groups = Group::find_by_organization(org_id, conn).await;
let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect();
if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(&g.id)) {
err!("Invalid group", format!("Group {} does not belong to organization {}!", e.id, org_id))
}
let org_memberships = Membership::find_by_org(org_id, conn).await;
let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect();
if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(&m.id)) {
err!("Invalid member", format!("Member {} does not belong to organization {}!", e.id, org_id))
}
Ok(())
}
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct CollectionGroupData { struct CollectionGroupData {
@@ -233,11 +251,13 @@ async fn post_delete_organization(
} }
#[post("/organizations/<org_id>/leave")] #[post("/organizations/<org_id>/leave")]
async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbConn) -> EmptyResult { async fn leave_organization(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> EmptyResult {
match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { if headers.membership.status != MembershipStatus::Confirmed as i32 {
None => err!("User not part of organization"), err!("You need to be a Member of the Organization to call this endpoint")
Some(member) => { }
if member.atype == MembershipType::Owner let membership = headers.membership;
if membership.atype == MembershipType::Owner
&& Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 && Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1
{ {
err!("The last owner can't leave") err!("The last owner can't leave")
@@ -245,7 +265,7 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbCo
log_event( log_event(
EventType::OrganizationUserLeft as i32, EventType::OrganizationUserLeft as i32,
&member.uuid, &membership.uuid,
&org_id, &org_id,
&headers.user.uuid, &headers.user.uuid,
headers.device.atype, headers.device.atype,
@@ -254,9 +274,7 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbCo
) )
.await; .await;
member.delete(&conn).await membership.delete(&conn).await
}
}
} }
#[get("/organizations/<org_id>")] #[get("/organizations/<org_id>")]
@@ -480,12 +498,9 @@ async fn post_organization_collections(
err!("Organization not found", "Organization id's do not match"); err!("Organization not found", "Organization id's do not match");
} }
let data: FullCollectionData = data.into_inner(); let data: FullCollectionData = data.into_inner();
data.validate(&org_id, &conn).await?;
let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else { let collection = Collection::new(org_id.clone(), data.name, data.external_id);
err!("Can't find organization details")
};
let collection = Collection::new(org.uuid, data.name, data.external_id);
collection.save(&conn).await?; collection.save(&conn).await?;
log_event( log_event(
@@ -501,7 +516,7 @@ async fn post_organization_collections(
for group in data.groups { for group in data.groups {
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage) CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
.save(&conn) .save(&org_id, &conn)
.await?; .await?;
} }
@@ -579,10 +594,10 @@ async fn post_bulk_access_collections(
) )
.await; .await;
CollectionGroup::delete_all_by_collection(&col_id, &conn).await?; CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?;
for group in &data.groups { for group in &data.groups {
CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage) CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage)
.save(&conn) .save(&org_id, &conn)
.await?; .await?;
} }
@@ -627,6 +642,7 @@ async fn post_organization_collection_update(
err!("Organization not found", "Organization id's do not match"); err!("Organization not found", "Organization id's do not match");
} }
let data: FullCollectionData = data.into_inner(); let data: FullCollectionData = data.into_inner();
data.validate(&org_id, &conn).await?;
if Organization::find_by_uuid(&org_id, &conn).await.is_none() { if Organization::find_by_uuid(&org_id, &conn).await.is_none() {
err!("Can't find organization details") err!("Can't find organization details")
@@ -655,11 +671,11 @@ async fn post_organization_collection_update(
) )
.await; .await;
CollectionGroup::delete_all_by_collection(&col_id, &conn).await?; CollectionGroup::delete_all_by_collection(&col_id, &org_id, &conn).await?;
for group in data.groups { for group in data.groups {
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage) CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
.save(&conn) .save(&org_id, &conn)
.await?; .await?;
} }
@@ -1003,6 +1019,24 @@ struct InviteData {
permissions: HashMap<String, Value>, permissions: HashMap<String, Value>,
} }
impl InviteData {
async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
let org_collections = Collection::find_by_organization(org_id, conn).await;
let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect();
if let Some(e) = self.collections.iter().flatten().find(|c| !org_collection_ids.contains(&c.id)) {
err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id))
}
let org_groups = Group::find_by_organization(org_id, conn).await;
let org_group_ids: HashSet<&GroupId> = org_groups.iter().map(|c| &c.uuid).collect();
if let Some(e) = self.groups.iter().find(|g| !org_group_ids.contains(g)) {
err!("Invalid group", format!("Group {} does not belong to organization {}!", e, org_id))
}
Ok(())
}
}
#[post("/organizations/<org_id>/users/invite", data = "<data>")] #[post("/organizations/<org_id>/users/invite", data = "<data>")]
async fn send_invite( async fn send_invite(
org_id: OrganizationId, org_id: OrganizationId,
@@ -1014,6 +1048,7 @@ async fn send_invite(
err!("Organization not found", "Organization id's do not match"); err!("Organization not found", "Organization id's do not match");
} }
let data: InviteData = data.into_inner(); let data: InviteData = data.into_inner();
data.validate(&org_id, &conn).await?;
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
// The from_str() will convert the custom role type into a manager role type // The from_str() will convert the custom role type into a manager role type
@@ -1273,20 +1308,20 @@ async fn accept_invite(
// skip invitation logic when we were invited via the /admin panel // skip invitation logic when we were invited via the /admin panel
if **member_id != FAKE_ADMIN_UUID { if **member_id != FAKE_ADMIN_UUID {
let Some(mut member) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else { let Some(mut membership) = Membership::find_by_uuid_and_org(member_id, &claims.org_id, &conn).await else {
err!("Error accepting the invitation") err!("Error accepting the invitation")
}; };
let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&member.org_uuid, &conn).await { let reset_password_key = match OrgPolicy::org_is_reset_password_auto_enroll(&membership.org_uuid, &conn).await {
true if data.reset_password_key.is_none() => err!("Reset password key is required, but not provided."), true if data.reset_password_key.is_none() => err!("Reset password key is required, but not provided."),
true => data.reset_password_key, true => data.reset_password_key,
false => None, false => None,
}; };
// In case the user was invited before the mail was saved in db. // In case the user was invited before the mail was saved in db.
member.invited_by_email = member.invited_by_email.or(claims.invited_by_email); membership.invited_by_email = membership.invited_by_email.or(claims.invited_by_email);
accept_org_invite(&headers.user, member, reset_password_key, &conn).await?; accept_org_invite(&headers.user, membership, reset_password_key, &conn).await?;
} else if CONFIG.mail_enabled() { } else if CONFIG.mail_enabled() {
// User was invited from /admin, so they are automatically confirmed // User was invited from /admin, so they are automatically confirmed
let org_name = CONFIG.invitation_org_name(); let org_name = CONFIG.invitation_org_name();
@@ -1520,9 +1555,8 @@ async fn edit_member(
&& data.permissions.get("deleteAnyCollection") == Some(&json!(true)) && data.permissions.get("deleteAnyCollection") == Some(&json!(true))
&& data.permissions.get("createNewCollections") == Some(&json!(true))); && data.permissions.get("createNewCollections") == Some(&json!(true)));
let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await { let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else {
Some(member) => member, err!("The specified user isn't member of the organization")
None => err!("The specified user isn't member of the organization"),
}; };
if new_type != member_to_edit.atype if new_type != member_to_edit.atype
@@ -1839,7 +1873,6 @@ async fn post_org_import(
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct BulkCollectionsData { struct BulkCollectionsData {
organization_id: OrganizationId, organization_id: OrganizationId,
cipher_ids: Vec<CipherId>, cipher_ids: Vec<CipherId>,
@@ -1853,6 +1886,10 @@ struct BulkCollectionsData {
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, conn: DbConn) -> EmptyResult { async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: BulkCollectionsData = data.into_inner(); let data: BulkCollectionsData = data.into_inner();
if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &conn).await.is_none() {
err!("You need to be a Member of the Organization to call this endpoint")
}
// Get all the collection available to the user in one query // Get all the collection available to the user in one query
// Also filter based upon the provided collections // Also filter based upon the provided collections
let user_collections: HashMap<CollectionId, Collection> = let user_collections: HashMap<CollectionId, Collection> =
@@ -1941,7 +1978,7 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn)
// Called during the SSO enrollment. // Called during the SSO enrollment.
// Return the org policy if it exists, otherwise use the default one. // Return the org policy if it exists, otherwise use the default one.
#[get("/organizations/<org_id>/policies/master-password", rank = 1)] #[get("/organizations/<org_id>/policies/master-password", rank = 1)]
async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, conn: DbConn) -> JsonResult { async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
let policy = let policy =
OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| { OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::MasterPassword, &conn).await.unwrap_or_else(|| {
let (enabled, data) = match CONFIG.sso_master_password_policy_value() { let (enabled, data) = match CONFIG.sso_master_password_policy_value() {
@@ -2149,13 +2186,13 @@ fn get_plans() -> Json<Value> {
} }
#[get("/organizations/<_org_id>/billing/metadata")] #[get("/organizations/<_org_id>/billing/metadata")]
fn get_billing_metadata(_org_id: OrganizationId, _headers: Headers) -> Json<Value> { fn get_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
// Prevent a 404 error, which also causes Javascript errors. // Prevent a 404 error, which also causes Javascript errors.
Json(_empty_data_json()) Json(_empty_data_json())
} }
#[get("/organizations/<_org_id>/billing/vnext/warnings")] #[get("/organizations/<_org_id>/billing/vnext/warnings")]
fn get_billing_warnings(_org_id: OrganizationId, _headers: Headers) -> Json<Value> { fn get_billing_warnings(_org_id: OrganizationId, _headers: OrgMemberHeaders) -> Json<Value> {
Json(json!({ Json(json!({
"freeTrial":null, "freeTrial":null,
"inactiveSubscription":null, "inactiveSubscription":null,
@@ -2427,6 +2464,23 @@ impl GroupRequest {
group group
} }
/// Validate if all the collections and members belong to the provided organization
pub async fn validate(&self, org_id: &OrganizationId, conn: &DbConn) -> EmptyResult {
let org_collections = Collection::find_by_organization(org_id, conn).await;
let org_collection_ids: HashSet<&CollectionId> = org_collections.iter().map(|c| &c.uuid).collect();
if let Some(e) = self.collections.iter().find(|c| !org_collection_ids.contains(&c.id)) {
err!("Invalid collection", format!("Collection {} does not belong to organization {}!", e.id, org_id))
}
let org_memberships = Membership::find_by_org(org_id, conn).await;
let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect();
if let Some(e) = self.users.iter().find(|m| !org_membership_ids.contains(m)) {
err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id))
}
Ok(())
}
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@@ -2470,6 +2524,8 @@ async fn post_groups(
} }
let group_request = data.into_inner(); let group_request = data.into_inner();
group_request.validate(&org_id, &conn).await?;
let group = group_request.to_group(&org_id); let group = group_request.to_group(&org_id);
log_event( log_event(
@@ -2506,10 +2562,12 @@ async fn put_group(
}; };
let group_request = data.into_inner(); let group_request = data.into_inner();
group_request.validate(&org_id, &conn).await?;
let updated_group = group_request.update_group(group); let updated_group = group_request.update_group(group);
CollectionGroup::delete_all_by_group(&group_id, &conn).await?; CollectionGroup::delete_all_by_group(&group_id, &org_id, &conn).await?;
GroupUser::delete_all_by_group(&group_id, &conn).await?; GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?;
log_event( log_event(
EventType::GroupUpdated as i32, EventType::GroupUpdated as i32,
@@ -2537,7 +2595,7 @@ async fn add_update_group(
for col_selection in collections { for col_selection in collections {
let mut collection_group = col_selection.to_collection_group(group.uuid.clone()); let mut collection_group = col_selection.to_collection_group(group.uuid.clone());
collection_group.save(conn).await?; collection_group.save(&org_id, conn).await?;
} }
for assigned_member in members { for assigned_member in members {
@@ -2630,7 +2688,7 @@ async fn _delete_group(
) )
.await; .await;
group.delete(conn).await group.delete(org_id, conn).await
} }
#[delete("/organizations/<org_id>/groups", data = "<data>")] #[delete("/organizations/<org_id>/groups", data = "<data>")]
@@ -2689,7 +2747,7 @@ async fn get_group_members(
err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization")
}; };
let group_members: Vec<MembershipId> = GroupUser::find_by_group(&group_id, &conn) let group_members: Vec<MembershipId> = GroupUser::find_by_group(&group_id, &org_id, &conn)
.await .await
.iter() .iter()
.map(|entry| entry.users_organizations_uuid.clone()) .map(|entry| entry.users_organizations_uuid.clone())
@@ -2717,9 +2775,15 @@ async fn put_group_members(
err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization")
}; };
GroupUser::delete_all_by_group(&group_id, &conn).await?;
let assigned_members = data.into_inner(); let assigned_members = data.into_inner();
let org_memberships = Membership::find_by_org(&org_id, &conn).await;
let org_membership_ids: HashSet<&MembershipId> = org_memberships.iter().map(|m| &m.uuid).collect();
if let Some(e) = assigned_members.iter().find(|m| !org_membership_ids.contains(m)) {
err!("Invalid member", format!("Member {} does not belong to organization {}!", e, org_id))
}
GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?;
for assigned_member in assigned_members { for assigned_member in assigned_members {
let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone()); let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone());
user_entry.save(&conn).await?; user_entry.save(&conn).await?;
@@ -2951,15 +3015,20 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn)
Ok(()) Ok(())
} }
#[put("/organizations/<org_id>/users/<member_id>/reset-password-enrollment", data = "<data>")] #[put("/organizations/<org_id>/users/<user_id>/reset-password-enrollment", data = "<data>")]
async fn put_reset_password_enrollment( async fn put_reset_password_enrollment(
org_id: OrganizationId, org_id: OrganizationId,
member_id: MembershipId, user_id: UserId,
headers: Headers, headers: OrgMemberHeaders,
data: Json<OrganizationUserResetPasswordEnrollmentRequest>, data: Json<OrganizationUserResetPasswordEnrollmentRequest>,
conn: DbConn, conn: DbConn,
) -> EmptyResult { ) -> EmptyResult {
let Some(mut member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await else { if user_id != headers.user.uuid {
err!("User to enroll isn't member of required organization", "The user_id and acting user do not match");
}
let Some(mut membership) = Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &org_id, &conn).await
else {
err!("User to enroll isn't member of required organization") err!("User to enroll isn't member of required organization")
}; };
@@ -2986,16 +3055,17 @@ async fn put_reset_password_enrollment(
.await?; .await?;
} }
member.reset_password_key = reset_password_key; membership.reset_password_key = reset_password_key;
member.save(&conn).await?; membership.save(&conn).await?;
let log_id = if member.reset_password_key.is_some() { let event_type = if membership.reset_password_key.is_some() {
EventType::OrganizationUserResetPasswordEnroll as i32 EventType::OrganizationUserResetPasswordEnroll as i32
} else { } else {
EventType::OrganizationUserResetPasswordWithdraw as i32 EventType::OrganizationUserResetPasswordWithdraw as i32
}; };
log_event(log_id, &member_id, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn).await; log_event(event_type, &membership.uuid, &org_id, &headers.user.uuid, headers.device.atype, &headers.ip.ip, &conn)
.await;
Ok(()) Ok(())
} }

View File

@@ -156,7 +156,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, conn: DbConn
} }
}; };
GroupUser::delete_all_by_group(&group_uuid, &conn).await?; GroupUser::delete_all_by_group(&group_uuid, &org_id, &conn).await?;
for ext_id in &group_data.member_external_ids { for ext_id in &group_data.member_external_ids {
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await { if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &conn).await {

View File

@@ -704,10 +704,9 @@ pub struct OrgHeaders {
impl OrgHeaders { impl OrgHeaders {
fn is_member(&self) -> bool { fn is_member(&self) -> bool {
// NOTE: we don't care about MembershipStatus at the moment because this is only used // Only allow not revoked members, we can not use the Confirmed status here
// where an invited, accepted or confirmed user is expected if this ever changes or // as some endpoints can be triggered by invited users during joining
// if from_i32 is changed to return Some(Revoked) this check needs to be changed accordingly self.membership_status != MembershipStatus::Revoked && self.membership_type >= MembershipType::User
self.membership_type >= MembershipType::User
} }
fn is_confirmed_and_admin(&self) -> bool { fn is_confirmed_and_admin(&self) -> bool {
self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin self.membership_status == MembershipStatus::Confirmed && self.membership_type >= MembershipType::Admin
@@ -720,17 +719,10 @@ impl OrgHeaders {
} }
} }
#[rocket::async_trait] // org_id is usually the second path param ("/organizations/<org_id>"),
impl<'r> FromRequest<'r> for OrgHeaders { // but there are cases where it is a query value.
type Error = &'static str; // First check the path, if this is not a valid uuid, try the query values.
fn get_org_id(request: &Request<'_>) -> Option<OrganizationId> {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(Headers::from_request(request).await);
// org_id is usually the second path param ("/organizations/<org_id>"),
// but there are cases where it is a query value.
// First check the path, if this is not a valid uuid, try the query values.
let url_org_id: Option<OrganizationId> = {
if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) { if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) {
Some(org_id) Some(org_id)
} else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") { } else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") {
@@ -738,7 +730,34 @@ impl<'r> FromRequest<'r> for OrgHeaders {
} else { } else {
None None
} }
}; }
// Special Guard to ensure that there is an organization id present
// If there is no org id trigger the Outcome::Forward.
// This is useful for endpoints which work for both organization and personal vaults, like purge.
pub struct OrgIdGuard;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for OrgIdGuard {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match get_org_id(request) {
Some(_) => Outcome::Success(OrgIdGuard),
None => Outcome::Forward(rocket::http::Status::NotFound),
}
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for OrgHeaders {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(Headers::from_request(request).await);
// Extract the org_id from the request
let url_org_id = get_org_id(request);
match url_org_id { match url_org_id {
Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => { Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {

View File

@@ -559,7 +559,7 @@ impl Cipher {
if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) { if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {
return cached_member.has_full_access(); return cached_member.has_full_access();
} }
} else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await { } else if let Some(member) = Membership::find_confirmed_by_user_and_org(user_uuid, org_uuid, conn).await {
return member.has_full_access(); return member.has_full_access();
} }
} }
@@ -668,10 +668,12 @@ impl Cipher {
ciphers::table ciphers::table
.filter(ciphers::uuid.eq(&self.uuid)) .filter(ciphers::uuid.eq(&self.uuid))
.inner_join(ciphers_collections::table.on( .inner_join(ciphers_collections::table.on(
ciphers::uuid.eq(ciphers_collections::cipher_uuid))) ciphers::uuid.eq(ciphers_collections::cipher_uuid)
))
.inner_join(users_collections::table.on( .inner_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid)))) .and(users_collections::user_uuid.eq(user_uuid))
))
.select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
.load::<(bool, bool, bool)>(conn) .load::<(bool, bool, bool)>(conn)
.expect("Error getting user access restrictions") .expect("Error getting user access restrictions")
@@ -697,6 +699,9 @@ impl Cipher {
.inner_join(users_organizations::table.on( .inner_join(users_organizations::table.on(
users_organizations::uuid.eq(groups_users::users_organizations_uuid) users_organizations::uuid.eq(groups_users::users_organizations_uuid)
)) ))
.inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
))
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage)) .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
.load::<(bool, bool, bool)>(conn) .load::<(bool, bool, bool)>(conn)
@@ -809,13 +814,13 @@ impl Cipher {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) // Ensure that group and membership belong to the same org
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
collections_groups::groups_uuid.eq(groups::uuid) .and(collections_groups::groups_uuid.eq(groups::uuid))
)
)) ))
.filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner .filter(ciphers::user_uuid.eq(user_uuid)) // Cipher owner
.or_filter(users_organizations::access_all.eq(true)) // access_all in org .or_filter(users_organizations::access_all.eq(true)) // access_all in org
@@ -986,7 +991,9 @@ impl Cipher {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
.and(collections_groups::groups_uuid.eq(groups::uuid)) .and(collections_groups::groups_uuid.eq(groups::uuid))
@@ -1047,7 +1054,9 @@ impl Cipher {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid)
.and(collections_groups::groups_uuid.eq(groups::uuid)) .and(collections_groups::groups_uuid.eq(groups::uuid))
@@ -1115,8 +1124,8 @@ impl Cipher {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and( collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid).and(

View File

@@ -191,7 +191,7 @@ impl Collection {
self.update_users_revision(conn).await; self.update_users_revision(conn).await;
CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?; CollectionCipher::delete_all_by_collection(&self.uuid, conn).await?;
CollectionUser::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?; CollectionGroup::delete_all_by_collection(&self.uuid, &self.org_uuid, conn).await?;
db_run! { conn: { db_run! { conn: {
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
@@ -239,8 +239,8 @@ impl Collection {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
@@ -355,8 +355,8 @@ impl Collection {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
@@ -422,8 +422,8 @@ impl Collection {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid) collections_groups::groups_uuid.eq(groups_users::groups_uuid)
@@ -484,8 +484,8 @@ impl Collection {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
@@ -531,8 +531,8 @@ impl Collection {
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
)) ))
.left_join(groups::table.on( .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(

View File

@@ -1,6 +1,6 @@
use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId}; use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::db::schema::{collections_groups, groups, groups_users, users_organizations}; use crate::db::schema::{collections, collections_groups, groups, groups_users, users_organizations};
use crate::db::DbConn; use crate::db::DbConn;
use crate::error::MapResult; use crate::error::MapResult;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
@@ -81,7 +81,7 @@ impl Group {
// If both read_only and hide_passwords are false, then manage should be true // If both read_only and hide_passwords are false, then manage should be true
// You can't have an entry with read_only and manage, or hide_passwords and manage // You can't have an entry with read_only and manage, or hide_passwords and manage
// Or an entry with everything to false // Or an entry with everything to false
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn) let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, &self.organizations_uuid, conn)
.await .await
.iter() .iter()
.map(|entry| { .map(|entry| {
@@ -191,7 +191,7 @@ impl Group {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult { pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
for group in Self::find_by_organization(org_uuid, conn).await { for group in Self::find_by_organization(org_uuid, conn).await {
group.delete(conn).await?; group.delete(org_uuid, conn).await?;
} }
Ok(()) Ok(())
} }
@@ -246,8 +246,8 @@ impl Group {
.inner_join(users_organizations::table.on( .inner_join(users_organizations::table.on(
users_organizations::uuid.eq(groups_users::users_organizations_uuid) users_organizations::uuid.eq(groups_users::users_organizations_uuid)
)) ))
.inner_join(groups::table.on( .inner_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
groups::uuid.eq(groups_users::groups_uuid) .and(groups::organizations_uuid.eq(users_organizations::org_uuid))
)) ))
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.filter(groups::access_all.eq(true)) .filter(groups::access_all.eq(true))
@@ -276,9 +276,9 @@ impl Group {
}} }}
} }
pub async fn delete(&self, conn: &DbConn) -> EmptyResult { pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
CollectionGroup::delete_all_by_group(&self.uuid, conn).await?; CollectionGroup::delete_all_by_group(&self.uuid, org_uuid, conn).await?;
GroupUser::delete_all_by_group(&self.uuid, conn).await?; GroupUser::delete_all_by_group(&self.uuid, org_uuid, conn).await?;
db_run! { conn: { db_run! { conn: {
diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid))) diesel::delete(groups::table.filter(groups::uuid.eq(&self.uuid)))
@@ -306,8 +306,8 @@ impl Group {
} }
impl CollectionGroup { impl CollectionGroup {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { pub async fn save(&mut self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await; let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await;
for group_user in group_users { for group_user in group_users {
group_user.update_user_revision(conn).await; group_user.update_user_revision(conn).await;
} }
@@ -365,10 +365,19 @@ impl CollectionGroup {
} }
} }
pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> { pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: { db_run! { conn: {
collections_groups::table collections_groups::table
.inner_join(groups::table.on(
groups::uuid.eq(collections_groups::groups_uuid)
))
.inner_join(collections::table.on(
collections::uuid.eq(collections_groups::collections_uuid)
.and(collections::org_uuid.eq(groups::organizations_uuid))
))
.filter(collections_groups::groups_uuid.eq(group_uuid)) .filter(collections_groups::groups_uuid.eq(group_uuid))
.filter(collections::org_uuid.eq(org_uuid))
.select(collections_groups::all_columns)
.load::<Self>(conn) .load::<Self>(conn)
.expect("Error loading collection groups") .expect("Error loading collection groups")
}} }}
@@ -383,6 +392,13 @@ impl CollectionGroup {
.inner_join(users_organizations::table.on( .inner_join(users_organizations::table.on(
users_organizations::uuid.eq(groups_users::users_organizations_uuid) users_organizations::uuid.eq(groups_users::users_organizations_uuid)
)) ))
.inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
))
.inner_join(collections::table.on(
collections::uuid.eq(collections_groups::collections_uuid)
.and(collections::org_uuid.eq(groups::organizations_uuid))
))
.filter(users_organizations::user_uuid.eq(user_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.select(collections_groups::all_columns) .select(collections_groups::all_columns)
.load::<Self>(conn) .load::<Self>(conn)
@@ -394,14 +410,20 @@ impl CollectionGroup {
db_run! { conn: { db_run! { conn: {
collections_groups::table collections_groups::table
.filter(collections_groups::collections_uuid.eq(collection_uuid)) .filter(collections_groups::collections_uuid.eq(collection_uuid))
.inner_join(collections::table.on(
collections::uuid.eq(collections_groups::collections_uuid)
))
.inner_join(groups::table.on(groups::uuid.eq(collections_groups::groups_uuid)
.and(groups::organizations_uuid.eq(collections::org_uuid))
))
.select(collections_groups::all_columns) .select(collections_groups::all_columns)
.load::<Self>(conn) .load::<Self>(conn)
.expect("Error loading collection groups") .expect("Error loading collection groups")
}} }}
} }
pub async fn delete(&self, conn: &DbConn) -> EmptyResult { pub async fn delete(&self, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(&self.groups_uuid, conn).await; let group_users = GroupUser::find_by_group(&self.groups_uuid, org_uuid, conn).await;
for group_user in group_users { for group_user in group_users {
group_user.update_user_revision(conn).await; group_user.update_user_revision(conn).await;
} }
@@ -415,8 +437,8 @@ impl CollectionGroup {
}} }}
} }
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult { pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(group_uuid, conn).await; let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await;
for group_user in group_users { for group_user in group_users {
group_user.update_user_revision(conn).await; group_user.update_user_revision(conn).await;
} }
@@ -429,10 +451,14 @@ impl CollectionGroup {
}} }}
} }
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &DbConn) -> EmptyResult { pub async fn delete_all_by_collection(
collection_uuid: &CollectionId,
org_uuid: &OrganizationId,
conn: &DbConn,
) -> EmptyResult {
let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await; let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
for collection_assigned_to_group in collection_assigned_to_groups { 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; let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, org_uuid, conn).await;
for group_user in group_users { for group_user in group_users {
group_user.update_user_revision(conn).await; group_user.update_user_revision(conn).await;
} }
@@ -494,10 +520,19 @@ impl GroupUser {
} }
} }
pub async fn find_by_group(group_uuid: &GroupId, conn: &DbConn) -> Vec<Self> { pub async fn find_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
db_run! { conn: { db_run! { conn: {
groups_users::table groups_users::table
.inner_join(groups::table.on(
groups::uuid.eq(groups_users::groups_uuid)
))
.inner_join(users_organizations::table.on(
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
.and(users_organizations::org_uuid.eq(groups::organizations_uuid))
))
.filter(groups_users::groups_uuid.eq(group_uuid)) .filter(groups_users::groups_uuid.eq(group_uuid))
.filter(groups::organizations_uuid.eq(org_uuid))
.select(groups_users::all_columns)
.load::<Self>(conn) .load::<Self>(conn)
.expect("Error loading group users") .expect("Error loading group users")
}} }}
@@ -522,6 +557,13 @@ impl GroupUser {
.inner_join(collections_groups::table.on( .inner_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid) collections_groups::groups_uuid.eq(groups_users::groups_uuid)
)) ))
.inner_join(groups::table.on(
groups::uuid.eq(groups_users::groups_uuid)
))
.inner_join(collections::table.on(
collections::uuid.eq(collections_groups::collections_uuid)
.and(collections::org_uuid.eq(groups::organizations_uuid))
))
.filter(collections_groups::collections_uuid.eq(collection_uuid)) .filter(collections_groups::collections_uuid.eq(collection_uuid))
.filter(groups_users::users_organizations_uuid.eq(member_uuid)) .filter(groups_users::users_organizations_uuid.eq(member_uuid))
.count() .count()
@@ -575,8 +617,8 @@ impl GroupUser {
}} }}
} }
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &DbConn) -> EmptyResult { pub async fn delete_all_by_group(group_uuid: &GroupId, org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(group_uuid, conn).await; let group_users = GroupUser::find_by_group(group_uuid, org_uuid, conn).await;
for group_user in group_users { for group_user in group_users {
group_user.update_user_revision(conn).await; group_user.update_user_revision(conn).await;
} }

View File

@@ -332,7 +332,7 @@ impl OrgPolicy {
for policy in for policy in
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
{ {
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < MembershipType::Admin { if user.atype < MembershipType::Admin {
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) { match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
Ok(opts) => { Ok(opts) => {

View File

@@ -1073,7 +1073,9 @@ impl Membership {
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid) collections_groups::groups_uuid.eq(groups_users::groups_uuid)
)) ))
.left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid)
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))
))
.left_join(ciphers_collections::table.on( .left_join(ciphers_collections::table.on(
ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid)) ciphers_collections::collection_uuid.eq(collections_groups::collections_uuid).and(ciphers_collections::cipher_uuid.eq(&cipher_uuid))

View File

@@ -20,7 +20,6 @@ pub struct TwoFactor {
pub last_used: i64, pub last_used: i64,
} }
#[allow(dead_code)]
#[derive(num_derive::FromPrimitive)] #[derive(num_derive::FromPrimitive)]
pub enum TwoFactorType { pub enum TwoFactorType {
Authenticator = 0, Authenticator = 0,