Require confirmed membership for user access checks, not for admin views (#7763)

This commit is contained in:
Daniel García authored and GitHub committed 2026-09-23 23:22:43 +02:00
1 parent d2660324e6
commit 32098ca7d1
6 files changed
+68 -76

No files matched your search

+22 -58
View File
@@ -1,7 +1,4 @@
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
};
use std::cmp::Ordering;
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
@@ -24,8 +21,8 @@ use crate::{
use macros::UuidFromParam;
use super::{
Cipher, CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy,
OrgPolicyType, TwoFactor, User, UserId,
Cipher, CipherId, Collection, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType,
TwoFactor, User, UserId,
};
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@@ -536,7 +533,13 @@ impl Membership {
})
}
pub async fn to_json_user_details(&self, include_collections: bool, include_groups: bool, conn: &DbConn) -> Value {
// Used by admins to view other members, so nothing here may depend on this member's status
pub async fn to_json_details_for_admin(
&self,
include_collections: bool,
include_groups: bool,
conn: &DbConn,
) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
@@ -559,52 +562,23 @@ impl Membership {
// Check if a user is in a group which has access to all collections
// If that is the case, we should not return individual collections!
// This is used by admins to view other members, so it must not depend on the membership status
let full_access_group =
CONFIG.org_groups_enabled() && Group::is_in_full_access_group(&self.user_uuid, &self.org_uuid, conn).await;
CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&self.org_uuid, &self.uuid, conn).await;
// If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self
// Only the collections assigned directly are returned, the ones assigned via a group are returned via a special group endpoint
let collections: Vec<Value> = if include_collections && !(full_access_group || self.access_all) {
// Get all collections for the user here already to prevent more queries
let cu: HashMap<CollectionId, CollectionUser> =
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.map(|cu| (cu.collection_uuid.clone(), cu))
.collect();
// Get all collection groups for this user to prevent there inclusion
let cg: HashSet<CollectionId> = CollectionGroup::find_by_user(&self.user_uuid, conn)
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.map(|cg| cg.collections_uuid)
.collect();
Collection::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
.filter_map(|c| {
let (read_only, hide_passwords, manage) = if self.has_full_access() {
(false, false, self.atype >= MembershipType::Manager)
} else if let Some(cu) = cu.get(&c.uuid) {
(
cu.read_only,
cu.hide_passwords,
cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
)
// If previous checks failed it might be that this user has access via a group, but we should not return those elements here
// Those are returned via a special group endpoint
} else if cg.contains(&c.uuid) {
return None;
} else {
(true, true, false)
};
Some(json!({
"id": c.uuid,
"readOnly": read_only,
"hidePasswords": hide_passwords,
"manage": manage,
}))
.map(|cu| {
json!({
"id": cu.collection_uuid,
"readOnly": cu.read_only,
"hidePasswords": cu.hide_passwords,
"manage": cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords),
})
})
.collect()
} else {
@@ -1032,17 +1006,6 @@ impl Membership {
.await
}
pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<OrganizationId> {
conn.run(move |conn| {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.select(users_organizations::org_uuid)
.load::<OrganizationId>(conn)
.unwrap_or_default()
})
.await
}
pub async fn find_by_user_and_policy(user_uuid: &UserId, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| {
users_organizations::table
@@ -1126,6 +1089,7 @@ impl Membership {
.and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))),
)
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(
users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]),
)