|
|
|
@ -15,8 +15,8 @@ use crate::{ |
|
|
|
db::{ |
|
|
|
DbConn, |
|
|
|
schema::{ |
|
|
|
ciphers, ciphers_collections, collections_groups, groups, groups_users, org_policies, organization_api_key, |
|
|
|
organizations, users, users_collections, users_organizations, |
|
|
|
ciphers, ciphers_collections, collections, collections_groups, groups, groups_users, org_policies, |
|
|
|
organization_api_key, organizations, users, users_collections, users_organizations, |
|
|
|
}, |
|
|
|
}, |
|
|
|
error::MapResult, |
|
|
|
@ -44,6 +44,7 @@ pub struct Organization { |
|
|
|
#[diesel(table_name = users_organizations)] |
|
|
|
#[diesel(treat_none_as_null = true)] |
|
|
|
#[diesel(primary_key(uuid))] |
|
|
|
#[allow(clippy::struct_excessive_bools)] |
|
|
|
pub struct Membership { |
|
|
|
pub uuid: MembershipId, |
|
|
|
pub user_uuid: UserId, |
|
|
|
@ -57,6 +58,12 @@ pub struct Membership { |
|
|
|
pub atype: i32, |
|
|
|
pub reset_password_key: Option<String>, |
|
|
|
pub external_id: Option<String>, |
|
|
|
pub manage_users: bool, |
|
|
|
pub manage_groups: bool, |
|
|
|
pub manage_policies: bool, |
|
|
|
pub create_new_collections: bool, |
|
|
|
pub edit_any_collection: bool, |
|
|
|
pub delete_any_collection: bool, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)] |
|
|
|
@ -98,36 +105,39 @@ pub enum MembershipType { |
|
|
|
Admin = 1, |
|
|
|
User = 2, |
|
|
|
Manager = 3, |
|
|
|
Custom = 4, |
|
|
|
} |
|
|
|
|
|
|
|
impl MembershipType { |
|
|
|
pub fn from_str(s: &str) -> Option<Self> { |
|
|
|
#[expect(
|
|
|
|
clippy::match_same_arms, |
|
|
|
reason = "Specifically define `4|Custom` since this is a hack, not a default" |
|
|
|
)] |
|
|
|
match s { |
|
|
|
"0" | "Owner" => Some(MembershipType::Owner), |
|
|
|
"1" | "Admin" => Some(MembershipType::Admin), |
|
|
|
"2" | "User" => Some(MembershipType::User), |
|
|
|
"3" | "Manager" => Some(MembershipType::Manager), |
|
|
|
// HACK: We convert the custom role to a manager role
|
|
|
|
"4" | "Custom" => Some(MembershipType::Manager), |
|
|
|
"4" | "Custom" => Some(MembershipType::Custom), |
|
|
|
_ => None, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const fn access_rank(self) -> u8 { |
|
|
|
match self { |
|
|
|
Self::User => 0, |
|
|
|
Self::Manager | Self::Custom => 1, |
|
|
|
Self::Admin => 2, |
|
|
|
Self::Owner => 3, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
impl Ord for MembershipType { |
|
|
|
fn cmp(&self, other: &MembershipType) -> Ordering { |
|
|
|
// For easy comparison, map each variant to an access level (where 0 is lowest).
|
|
|
|
const ACCESS_LEVEL: [i32; 4] = [ |
|
|
|
3, // Owner
|
|
|
|
2, // Admin
|
|
|
|
0, // User
|
|
|
|
1, // Manager && Custom
|
|
|
|
]; |
|
|
|
ACCESS_LEVEL[*self as usize].cmp(&ACCESS_LEVEL[*other as usize]) |
|
|
|
// Manager and Custom intentionally share the same authorization rank. A total ordering
|
|
|
|
// still has to distinguish unequal enum variants, otherwise `Ord` would disagree with
|
|
|
|
// `Eq` and ordered maps/sets could collapse one role into the other. The discriminant is a
|
|
|
|
// stable tie-breaker and places Custom after Manager, preserving `Custom >= Manager` while
|
|
|
|
// keeping both roles below Admin.
|
|
|
|
self.access_rank().cmp(&other.access_rank()).then_with(|| (*self as i32).cmp(&(*other as i32))) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -267,6 +277,12 @@ impl Membership { |
|
|
|
atype: MembershipType::User as i32, |
|
|
|
reset_password_key: None, |
|
|
|
external_id: None, |
|
|
|
manage_users: false, |
|
|
|
manage_groups: false, |
|
|
|
manage_policies: false, |
|
|
|
create_new_collections: false, |
|
|
|
edit_any_collection: false, |
|
|
|
delete_any_collection: false, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -306,15 +322,6 @@ impl Membership { |
|
|
|
} |
|
|
|
false |
|
|
|
} |
|
|
|
|
|
|
|
/// HACK: Convert the manager type to a custom type
|
|
|
|
/// It will be converted back on other locations
|
|
|
|
pub fn type_manager_as_custom(&self) -> i32 { |
|
|
|
match self.atype { |
|
|
|
3 => 4, |
|
|
|
_ => self.atype, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
impl OrganizationApiKey { |
|
|
|
@ -443,29 +450,29 @@ impl Membership { |
|
|
|
pub async fn to_json(&self, conn: &DbConn) -> Value { |
|
|
|
let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap(); |
|
|
|
|
|
|
|
// HACK: Convert the manager type to a custom type
|
|
|
|
// It will be converted back on other locations
|
|
|
|
let membership_type = self.type_manager_as_custom(); |
|
|
|
let membership_type = self.atype; |
|
|
|
|
|
|
|
let permissions = json!({ |
|
|
|
// TODO: Add full support for Custom User Roles
|
|
|
|
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
|
|
|
// Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission
|
|
|
|
"accessEventLogs": false, |
|
|
|
"accessImportExport": false, |
|
|
|
"accessReports": false, |
|
|
|
// If the following 3 Collection roles are set to true a custom user has access all permission
|
|
|
|
"createNewCollections": membership_type == 4 && self.access_all, |
|
|
|
"editAnyCollection": membership_type == 4 && self.access_all, |
|
|
|
"deleteAnyCollection": membership_type == 4 && self.access_all, |
|
|
|
"manageGroups": false, |
|
|
|
"managePolicies": false, |
|
|
|
"createNewCollections": membership_type == MembershipType::Custom as i32 && self.create_new_collections, |
|
|
|
"editAnyCollection": membership_type == MembershipType::Custom as i32 && self.edit_any_collection, |
|
|
|
"deleteAnyCollection": membership_type == MembershipType::Custom as i32 && self.delete_any_collection, |
|
|
|
"manageGroups": membership_type == MembershipType::Custom as i32 && self.manage_groups, |
|
|
|
"managePolicies": membership_type == MembershipType::Custom as i32 && self.manage_policies, |
|
|
|
"manageSso": false, // Not supported
|
|
|
|
"manageUsers": false, |
|
|
|
"manageUsers": membership_type == MembershipType::Custom as i32 && self.manage_users, |
|
|
|
"manageResetPassword": false, |
|
|
|
"manageScim": false // Not supported (Not AGPLv3 Licensed)
|
|
|
|
}); |
|
|
|
|
|
|
|
// edit_any_collection is internally mirrored to access_all to provide Bitwarden-compatible
|
|
|
|
// cipher access, but it must not accidentally grant collection creation. The client treats
|
|
|
|
// limitCollectionCreation=false as an independent create grant, so compute it from the
|
|
|
|
// actual role/permission rather than access_all for Custom members.
|
|
|
|
let limit_collection_creation = self.limit_collection_creation(); |
|
|
|
|
|
|
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs
|
|
|
|
json!({ |
|
|
|
"id": self.org_uuid, |
|
|
|
@ -509,8 +516,7 @@ impl Membership { |
|
|
|
"familySponsorshipValidUntil": null, |
|
|
|
"familySponsorshipToDelete": null, |
|
|
|
"accessSecretsManager": false, |
|
|
|
// limit collection creation to managers with access_all permission to prevent issues
|
|
|
|
"limitCollectionCreation": self.atype < MembershipType::Manager || !self.access_all, |
|
|
|
"limitCollectionCreation": limit_collection_creation, |
|
|
|
"limitCollectionDeletion": true, |
|
|
|
"limitItemDeletion": false, |
|
|
|
"allowAdminAccessToAllCollectionItems": true, |
|
|
|
@ -585,7 +591,7 @@ impl Membership { |
|
|
|
( |
|
|
|
cu.read_only, |
|
|
|
cu.hide_passwords, |
|
|
|
cu.manage || (self.atype == MembershipType::Manager && !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
|
|
|
|
@ -607,28 +613,22 @@ impl Membership { |
|
|
|
Vec::new() |
|
|
|
}; |
|
|
|
|
|
|
|
// HACK: Convert the manager type to a custom type
|
|
|
|
// It will be converted back on other locations
|
|
|
|
let membership_type = self.type_manager_as_custom(); |
|
|
|
let membership_type = self.atype; |
|
|
|
|
|
|
|
// HACK: Only return permissions if the user is of type custom and has access_all
|
|
|
|
// Else Bitwarden will assume the defaults of all false
|
|
|
|
let permissions = if membership_type == 4 && self.access_all { |
|
|
|
// Only return a permissions object for custom-type members. Otherwise Bitwarden assumes
|
|
|
|
// all-false defaults and the role itself supplies any elevated capabilities.
|
|
|
|
let permissions = if membership_type == MembershipType::Custom as i32 { |
|
|
|
json!({ |
|
|
|
// TODO: Add full support for Custom User Roles
|
|
|
|
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
|
|
|
// Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission
|
|
|
|
"accessEventLogs": false, |
|
|
|
"accessImportExport": false, |
|
|
|
"accessReports": false, |
|
|
|
// If the following 3 Collection roles are set to true a custom user has access all permission
|
|
|
|
"createNewCollections": true, |
|
|
|
"editAnyCollection": true, |
|
|
|
"deleteAnyCollection": true, |
|
|
|
"manageGroups": false, |
|
|
|
"managePolicies": false, |
|
|
|
"createNewCollections": self.create_new_collections, |
|
|
|
"editAnyCollection": self.edit_any_collection, |
|
|
|
"deleteAnyCollection": self.delete_any_collection, |
|
|
|
"manageGroups": self.manage_groups, |
|
|
|
"managePolicies": self.manage_policies, |
|
|
|
"manageSso": false, // Not supported
|
|
|
|
"manageUsers": false, |
|
|
|
"manageUsers": self.manage_users, |
|
|
|
"manageResetPassword": false, |
|
|
|
"manageScim": false // Not supported (Not AGPLv3 Licensed)
|
|
|
|
}) |
|
|
|
@ -728,7 +728,7 @@ impl Membership { |
|
|
|
json!({ |
|
|
|
"id": self.uuid, |
|
|
|
"userId": self.user_uuid, |
|
|
|
"type": self.type_manager_as_custom(), // HACK: Convert the manager type to a custom type
|
|
|
|
"type": self.atype, |
|
|
|
"status": status, |
|
|
|
"name": user.name, |
|
|
|
"email": user.email, |
|
|
|
@ -816,7 +816,143 @@ impl Membership { |
|
|
|
} |
|
|
|
|
|
|
|
pub fn has_full_access(&self) -> bool { |
|
|
|
(self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed) |
|
|
|
(self.access_all || self.has_edit_any_collection() || self.atype >= MembershipType::Admin) |
|
|
|
&& self.has_status(MembershipStatus::Confirmed) |
|
|
|
} |
|
|
|
|
|
|
|
// The granular custom permission flags are only meaningful while the membership is of
|
|
|
|
// the Custom type. Gating them on the type here ensures that a stale flag left over from
|
|
|
|
// a type change (e.g. via the admin panel) can never grant anything.
|
|
|
|
pub fn has_manage_users(&self) -> bool { |
|
|
|
self.has_type(MembershipType::Custom) && self.manage_users |
|
|
|
} |
|
|
|
|
|
|
|
pub fn has_manage_groups(&self) -> bool { |
|
|
|
self.has_type(MembershipType::Custom) && self.manage_groups |
|
|
|
} |
|
|
|
|
|
|
|
pub fn has_manage_policies(&self) -> bool { |
|
|
|
self.has_type(MembershipType::Custom) && self.manage_policies |
|
|
|
} |
|
|
|
|
|
|
|
pub fn has_create_new_collections(&self) -> bool { |
|
|
|
self.has_type(MembershipType::Custom) && self.create_new_collections |
|
|
|
} |
|
|
|
|
|
|
|
pub fn has_edit_any_collection(&self) -> bool { |
|
|
|
self.has_type(MembershipType::Custom) && self.edit_any_collection |
|
|
|
} |
|
|
|
|
|
|
|
pub fn has_delete_any_collection(&self) -> bool { |
|
|
|
self.has_type(MembershipType::Custom) && self.delete_any_collection |
|
|
|
} |
|
|
|
|
|
|
|
/// Check for an explicit per-collection Manage grant without treating any `access_all` value
|
|
|
|
/// as such a grant. Custom-role collection guards use this instead of the legacy broad helper,
|
|
|
|
/// because membership/group `access_all` must not manufacture a per-collection Manage grant.
|
|
|
|
pub async fn has_explicit_collection_manage_access(&self, collection_uuid: &CollectionId, conn: &DbConn) -> bool { |
|
|
|
let membership_uuid = self.uuid.clone(); |
|
|
|
let user_uuid = self.user_uuid.clone(); |
|
|
|
let org_uuid = self.org_uuid.clone(); |
|
|
|
let collection_uuid = collection_uuid.clone(); |
|
|
|
|
|
|
|
conn.run(move |conn| { |
|
|
|
let has_direct_manage = users_organizations::table |
|
|
|
.inner_join( |
|
|
|
users_collections::table.on(users_collections::user_uuid.eq(users_organizations::user_uuid)), |
|
|
|
) |
|
|
|
.inner_join( |
|
|
|
collections::table.on(collections::uuid |
|
|
|
.eq(users_collections::collection_uuid) |
|
|
|
.and(collections::org_uuid.eq(users_organizations::org_uuid))), |
|
|
|
) |
|
|
|
.filter(users_organizations::uuid.eq(membership_uuid.clone())) |
|
|
|
.filter(users_organizations::user_uuid.eq(user_uuid.clone())) |
|
|
|
.filter(users_organizations::org_uuid.eq(org_uuid.clone())) |
|
|
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) |
|
|
|
.filter(collections::uuid.eq(collection_uuid.clone())) |
|
|
|
.filter(users_collections::manage.eq(true)) |
|
|
|
.count() |
|
|
|
.first::<i64>(conn) |
|
|
|
.unwrap_or(0) |
|
|
|
!= 0; |
|
|
|
|
|
|
|
if has_direct_manage { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
users_organizations::table |
|
|
|
.inner_join( |
|
|
|
groups_users::table.on(groups_users::users_organizations_uuid.eq(users_organizations::uuid)), |
|
|
|
) |
|
|
|
.inner_join( |
|
|
|
groups::table.on(groups::uuid |
|
|
|
.eq(groups_users::groups_uuid) |
|
|
|
.and(groups::organizations_uuid.eq(users_organizations::org_uuid))), |
|
|
|
) |
|
|
|
.inner_join(collections_groups::table.on(collections_groups::groups_uuid.eq(groups_users::groups_uuid))) |
|
|
|
.inner_join( |
|
|
|
collections::table.on(collections::uuid |
|
|
|
.eq(collections_groups::collections_uuid) |
|
|
|
.and(collections::org_uuid.eq(users_organizations::org_uuid))), |
|
|
|
) |
|
|
|
.filter(users_organizations::uuid.eq(membership_uuid)) |
|
|
|
.filter(users_organizations::user_uuid.eq(user_uuid)) |
|
|
|
.filter(users_organizations::org_uuid.eq(org_uuid)) |
|
|
|
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32)) |
|
|
|
.filter(collections::uuid.eq(collection_uuid)) |
|
|
|
.filter(collections_groups::manage.eq(true)) |
|
|
|
.count() |
|
|
|
.first::<i64>(conn) |
|
|
|
.unwrap_or(0) |
|
|
|
!= 0 |
|
|
|
}) |
|
|
|
.await |
|
|
|
} |
|
|
|
|
|
|
|
/// `manageAllCollections` is a client-side aggregate checkbox, not a separately persisted
|
|
|
|
/// Bitwarden permission. It is selected exactly when all three child permissions are selected.
|
|
|
|
pub fn has_manage_all_collections(&self) -> bool { |
|
|
|
self.has_create_new_collections() && self.has_edit_any_collection() && self.has_delete_any_collection() |
|
|
|
} |
|
|
|
|
|
|
|
/// Match Vaultwarden's existing collection-creation policy while keeping the new Custom
|
|
|
|
/// permission independent from edit/delete. Legacy Manager memberships retain their former
|
|
|
|
/// access_all-based behavior.
|
|
|
|
pub fn can_create_new_collections(&self) -> bool { |
|
|
|
if !self.has_status(MembershipStatus::Confirmed) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
match MembershipType::from_i32(self.atype) { |
|
|
|
Some(MembershipType::Owner | MembershipType::Admin) => true, |
|
|
|
Some(MembershipType::Manager) => self.access_all, |
|
|
|
Some(MembershipType::Custom) => self.create_new_collections, |
|
|
|
Some(MembershipType::User) | None => false, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pub fn limit_collection_creation(&self) -> bool { |
|
|
|
match MembershipType::from_i32(self.atype) { |
|
|
|
Some(MembershipType::Owner | MembershipType::Admin) => false, |
|
|
|
Some(MembershipType::Manager) => !self.access_all, |
|
|
|
Some(MembershipType::Custom) => !self.create_new_collections, |
|
|
|
Some(MembershipType::User) | None => true, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pub fn can_delete_any_collection(&self) -> bool { |
|
|
|
self.has_status(MembershipStatus::Confirmed) |
|
|
|
&& (self.atype >= MembershipType::Admin || self.has_delete_any_collection()) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn clear_custom_permissions(&mut self) { |
|
|
|
self.manage_users = false; |
|
|
|
self.manage_groups = false; |
|
|
|
self.manage_policies = false; |
|
|
|
self.create_new_collections = false; |
|
|
|
self.edit_any_collection = false; |
|
|
|
self.delete_any_collection = false; |
|
|
|
} |
|
|
|
|
|
|
|
pub async fn find_by_uuid(uuid: &MembershipId, conn: &DbConn) -> Option<Self> { |
|
|
|
@ -925,7 +1061,7 @@ impl Membership { |
|
|
|
.await |
|
|
|
} |
|
|
|
|
|
|
|
// Get all users which are either owner or admin, or a manager which can manage/access all
|
|
|
|
// Get all users which are either owner or admin, or a manager/custom member which can manage/access all
|
|
|
|
pub async fn find_confirmed_and_manage_all_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> { |
|
|
|
conn.run(move |conn| { |
|
|
|
users_organizations::table |
|
|
|
@ -935,7 +1071,7 @@ impl Membership { |
|
|
|
users_organizations::atype |
|
|
|
.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]) |
|
|
|
.or(users_organizations::atype |
|
|
|
.eq(MembershipType::Manager as i32) |
|
|
|
.eq_any(vec![MembershipType::Manager as i32, MembershipType::Custom as i32]) |
|
|
|
.and(users_organizations::access_all.eq(true))), |
|
|
|
) |
|
|
|
.load::<Self>(conn) |
|
|
|
@ -1264,12 +1400,128 @@ pub struct OrgApiKeyId(String); |
|
|
|
mod tests { |
|
|
|
use super::*; |
|
|
|
|
|
|
|
fn membership(member_type: MembershipType) -> Membership { |
|
|
|
let mut membership = Membership::new("test-user".to_owned().into(), "test-org".to_owned().into(), None); |
|
|
|
membership.atype = member_type as i32; |
|
|
|
membership.status = MembershipStatus::Confirmed as i32; |
|
|
|
membership |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
#[allow(non_snake_case)] |
|
|
|
fn partial_cmp_MembershipType() { |
|
|
|
fn membership_type_order_preserves_access_rank_and_ord_contract() { |
|
|
|
assert!(MembershipType::Owner > MembershipType::Admin); |
|
|
|
assert!(MembershipType::Admin > MembershipType::Manager); |
|
|
|
assert!(MembershipType::Admin > MembershipType::Custom); |
|
|
|
assert!(MembershipType::Custom > MembershipType::Manager); |
|
|
|
assert!(MembershipType::Manager > MembershipType::User); |
|
|
|
assert!(MembershipType::Manager == MembershipType::from_str("4").unwrap()); |
|
|
|
assert!(MembershipType::Custom == MembershipType::from_str("4").unwrap()); |
|
|
|
|
|
|
|
// Permission comparisons continue to treat Custom as manager-level and below Admin.
|
|
|
|
assert!(MembershipType::Custom >= MembershipType::Manager); |
|
|
|
let custom = MembershipType::Custom as i32; |
|
|
|
assert!(custom >= MembershipType::Manager); |
|
|
|
assert!(custom < MembershipType::Admin); |
|
|
|
|
|
|
|
let types = [ |
|
|
|
MembershipType::Owner, |
|
|
|
MembershipType::Admin, |
|
|
|
MembershipType::User, |
|
|
|
MembershipType::Manager, |
|
|
|
MembershipType::Custom, |
|
|
|
]; |
|
|
|
for lhs in types { |
|
|
|
for rhs in types { |
|
|
|
assert_eq!(lhs.cmp(&rhs) == Ordering::Equal, lhs == rhs); |
|
|
|
assert_eq!(lhs.cmp(&rhs), rhs.cmp(&lhs).reverse()); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn custom_collection_permissions_are_independent_and_type_gated() { |
|
|
|
let mut member = membership(MembershipType::Custom); |
|
|
|
member.create_new_collections = true; |
|
|
|
|
|
|
|
assert!(member.has_create_new_collections()); |
|
|
|
assert!(member.can_create_new_collections()); |
|
|
|
assert!(!member.limit_collection_creation()); |
|
|
|
assert!(!member.has_full_access()); |
|
|
|
assert!(!member.can_delete_any_collection()); |
|
|
|
assert!(!member.has_manage_all_collections()); |
|
|
|
|
|
|
|
member.delete_any_collection = true; |
|
|
|
assert!(member.has_delete_any_collection()); |
|
|
|
assert!(member.can_delete_any_collection()); |
|
|
|
assert!(!member.has_full_access()); |
|
|
|
assert!(!member.has_manage_all_collections()); |
|
|
|
|
|
|
|
member.edit_any_collection = true; |
|
|
|
assert!(member.has_edit_any_collection()); |
|
|
|
assert!(member.has_full_access()); |
|
|
|
assert!(member.has_manage_all_collections()); |
|
|
|
|
|
|
|
// Stale flags on a non-Custom role are inert.
|
|
|
|
member.atype = MembershipType::User as i32; |
|
|
|
assert!(!member.has_create_new_collections()); |
|
|
|
assert!(!member.has_edit_any_collection()); |
|
|
|
assert!(!member.has_delete_any_collection()); |
|
|
|
assert!(!member.can_create_new_collections()); |
|
|
|
assert!(!member.can_delete_any_collection()); |
|
|
|
assert!(!member.has_full_access()); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn edit_any_collection_does_not_imply_create_or_delete() { |
|
|
|
let mut custom = membership(MembershipType::Custom); |
|
|
|
custom.edit_any_collection = true; |
|
|
|
// The persisted access_all mirror is intentionally tested too: client-facing create and
|
|
|
|
// delete decisions must still use their dedicated permissions.
|
|
|
|
custom.access_all = true; |
|
|
|
|
|
|
|
assert!(custom.has_full_access()); |
|
|
|
assert!(!custom.can_create_new_collections()); |
|
|
|
assert!(custom.limit_collection_creation()); |
|
|
|
assert!(!custom.can_delete_any_collection()); |
|
|
|
|
|
|
|
let mut manager = membership(MembershipType::Manager); |
|
|
|
manager.access_all = true; |
|
|
|
assert!(manager.can_create_new_collections()); |
|
|
|
|
|
|
|
let admin = membership(MembershipType::Admin); |
|
|
|
assert!(admin.can_create_new_collections()); |
|
|
|
assert!(!admin.limit_collection_creation()); |
|
|
|
assert!(admin.can_delete_any_collection()); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn custom_collection_permissions_require_confirmed_membership() { |
|
|
|
let mut member = membership(MembershipType::Custom); |
|
|
|
member.create_new_collections = true; |
|
|
|
member.edit_any_collection = true; |
|
|
|
member.delete_any_collection = true; |
|
|
|
member.status = MembershipStatus::Accepted as i32; |
|
|
|
|
|
|
|
assert!(!member.can_create_new_collections()); |
|
|
|
assert!(!member.can_delete_any_collection()); |
|
|
|
assert!(!member.has_full_access()); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn clearing_custom_permissions_clears_every_flag() { |
|
|
|
let mut member = membership(MembershipType::Custom); |
|
|
|
member.manage_users = true; |
|
|
|
member.manage_groups = true; |
|
|
|
member.manage_policies = true; |
|
|
|
member.create_new_collections = true; |
|
|
|
member.edit_any_collection = true; |
|
|
|
member.delete_any_collection = true; |
|
|
|
|
|
|
|
member.clear_custom_permissions(); |
|
|
|
|
|
|
|
assert!(!member.manage_users); |
|
|
|
assert!(!member.manage_groups); |
|
|
|
assert!(!member.manage_policies); |
|
|
|
assert!(!member.create_new_collections); |
|
|
|
assert!(!member.edit_any_collection); |
|
|
|
assert!(!member.delete_any_collection); |
|
|
|
} |
|
|
|
} |
|
|
|
|