|
|
@ -11,13 +11,18 @@ use crate::{ |
|
|
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, |
|
|
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, |
|
|
core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor}, |
|
|
core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor}, |
|
|
}, |
|
|
}, |
|
|
auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite}, |
|
|
auth::{ |
|
|
|
|
|
AccessImportExportHeaders, AdminHeaders, Headers, ManageGroupsHeaders, ManagePoliciesHeaders, |
|
|
|
|
|
ManageResetPasswordHeaders, ManageUsersHeaders, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, |
|
|
|
|
|
OwnerHeaders, decode_invite, |
|
|
|
|
|
}, |
|
|
db::{ |
|
|
db::{ |
|
|
DbConn, |
|
|
DbConn, |
|
|
models::{ |
|
|
models::{ |
|
|
Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, |
|
|
Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, |
|
|
Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, |
|
|
Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, |
|
|
OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId, |
|
|
OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, OrganizationUserPermissions, |
|
|
|
|
|
User, UserId, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
mail, |
|
|
mail, |
|
|
@ -107,6 +112,152 @@ pub fn routes() -> Vec<Route> { |
|
|
] |
|
|
] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn parse_custom_permissions( |
|
|
|
|
|
raw_type: &str, |
|
|
|
|
|
permissions: &HashMap<String, Value>, |
|
|
|
|
|
) -> Result<Option<OrganizationUserPermissions>, crate::Error> { |
|
|
|
|
|
if raw_type != "4" && raw_type != "Custom" { |
|
|
|
|
|
return Ok(None); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
match OrganizationUserPermissions::from_payload_map(permissions) { |
|
|
|
|
|
Ok(parsed) => Ok(Some(parsed)), |
|
|
|
|
|
Err(error) => err!(format!("Invalid custom role permissions payload: {error:#}")), |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn resolve_membership_custom_permissions( |
|
|
|
|
|
raw_type: &str, |
|
|
|
|
|
new_type: i32, |
|
|
|
|
|
permissions: &HashMap<String, Value>, |
|
|
|
|
|
) -> Result<(bool, Option<i32>), crate::Error> { |
|
|
|
|
|
let custom_permissions = parse_custom_permissions(raw_type, permissions)?; |
|
|
|
|
|
|
|
|
|
|
|
let access_all = new_type >= MembershipType::Admin |
|
|
|
|
|
|| custom_permissions.as_ref().is_some_and(OrganizationUserPermissions::has_manage_all_collections); |
|
|
|
|
|
|
|
|
|
|
|
let permissions_mask = custom_permissions.map(|custom_permissions| custom_permissions.to_mask()); |
|
|
|
|
|
|
|
|
|
|
|
Ok((access_all, permissions_mask)) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)] |
|
|
|
|
|
mod tests { |
|
|
|
|
|
use super::*; |
|
|
|
|
|
use crate::db::models::OrganizationUserPermission; |
|
|
|
|
|
|
|
|
|
|
|
fn custom_permissions_payload() -> HashMap<String, Value> { |
|
|
|
|
|
HashMap::from([ |
|
|
|
|
|
("accessEventLogs".to_owned(), json!(true)), |
|
|
|
|
|
("accessImportExport".to_owned(), json!(false)), |
|
|
|
|
|
("accessReports".to_owned(), json!(true)), |
|
|
|
|
|
("createNewCollections".to_owned(), json!(true)), |
|
|
|
|
|
("editAnyCollection".to_owned(), json!(true)), |
|
|
|
|
|
("deleteAnyCollection".to_owned(), json!(true)), |
|
|
|
|
|
("manageGroups".to_owned(), json!(true)), |
|
|
|
|
|
("managePolicies".to_owned(), json!(false)), |
|
|
|
|
|
("manageSso".to_owned(), json!(false)), |
|
|
|
|
|
("manageUsers".to_owned(), json!(true)), |
|
|
|
|
|
("manageResetPassword".to_owned(), json!(false)), |
|
|
|
|
|
("manageScim".to_owned(), json!(false)), |
|
|
|
|
|
]) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
fn invite_edit_store_custom_permissions_and_get_user_round_trip() { |
|
|
|
|
|
let payload = custom_permissions_payload(); |
|
|
|
|
|
|
|
|
|
|
|
let (access_all, permission_mask) = |
|
|
|
|
|
resolve_membership_custom_permissions("4", MembershipType::Manager as i32, &payload).unwrap(); |
|
|
|
|
|
|
|
|
|
|
|
assert!(access_all); |
|
|
|
|
|
|
|
|
|
|
|
let stored_mask = permission_mask.expect("custom role payload should be persisted"); |
|
|
|
|
|
let expected_mask = OrganizationUserPermission::AccessEventLogs.bit() |
|
|
|
|
|
| OrganizationUserPermission::AccessReports.bit() |
|
|
|
|
|
| OrganizationUserPermission::CreateNewCollections.bit() |
|
|
|
|
|
| OrganizationUserPermission::EditAnyCollection.bit() |
|
|
|
|
|
| OrganizationUserPermission::DeleteAnyCollection.bit() |
|
|
|
|
|
| OrganizationUserPermission::ManageGroups.bit() |
|
|
|
|
|
| OrganizationUserPermission::ManageUsers.bit(); |
|
|
|
|
|
assert_eq!(stored_mask, expected_mask); |
|
|
|
|
|
|
|
|
|
|
|
let parsed = OrganizationUserPermissions::from_mask(stored_mask); |
|
|
|
|
|
assert_eq!(stored_mask, parsed.to_mask()); |
|
|
|
|
|
|
|
|
|
|
|
assert_eq!( |
|
|
|
|
|
json!(parsed), |
|
|
|
|
|
json!({ |
|
|
|
|
|
"accessEventLogs": true, |
|
|
|
|
|
"accessImportExport": false, |
|
|
|
|
|
"accessReports": true, |
|
|
|
|
|
"createNewCollections": true, |
|
|
|
|
|
"editAnyCollection": true, |
|
|
|
|
|
"deleteAnyCollection": true, |
|
|
|
|
|
"manageGroups": true, |
|
|
|
|
|
"managePolicies": false, |
|
|
|
|
|
"manageSso": false, |
|
|
|
|
|
"manageUsers": true, |
|
|
|
|
|
"manageResetPassword": false, |
|
|
|
|
|
"manageScim": false |
|
|
|
|
|
}) |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
fn invite_edit_non_custom_role_keeps_permissions_unset() { |
|
|
|
|
|
let payload = custom_permissions_payload(); |
|
|
|
|
|
|
|
|
|
|
|
let (access_all, permission_mask) = |
|
|
|
|
|
resolve_membership_custom_permissions("3", MembershipType::Manager as i32, &payload).unwrap(); |
|
|
|
|
|
|
|
|
|
|
|
assert!(!access_all); |
|
|
|
|
|
assert!(permission_mask.is_none()); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
fn invite_edit_custom_partial_collection_permissions_do_not_grant_access_all() { |
|
|
|
|
|
let payload = HashMap::from([ |
|
|
|
|
|
("createNewCollections".to_owned(), json!(true)), |
|
|
|
|
|
("editAnyCollection".to_owned(), json!(true)), |
|
|
|
|
|
("deleteAnyCollection".to_owned(), json!(false)), |
|
|
|
|
|
("manageUsers".to_owned(), json!(true)), |
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
let (access_all, permission_mask) = |
|
|
|
|
|
resolve_membership_custom_permissions("Custom", MembershipType::Manager as i32, &payload).unwrap(); |
|
|
|
|
|
|
|
|
|
|
|
assert!(!access_all); |
|
|
|
|
|
|
|
|
|
|
|
let stored_mask = permission_mask.expect("custom role payload should be persisted"); |
|
|
|
|
|
let expected_mask = OrganizationUserPermission::CreateNewCollections.bit() |
|
|
|
|
|
| OrganizationUserPermission::EditAnyCollection.bit() |
|
|
|
|
|
| OrganizationUserPermission::ManageUsers.bit(); |
|
|
|
|
|
assert_eq!(stored_mask, expected_mask); |
|
|
|
|
|
|
|
|
|
|
|
let parsed = OrganizationUserPermissions::from_mask(stored_mask); |
|
|
|
|
|
|
|
|
|
|
|
assert!(parsed.manage_users); |
|
|
|
|
|
assert!(parsed.create_new_collections); |
|
|
|
|
|
assert!(parsed.edit_any_collection); |
|
|
|
|
|
assert!(!parsed.delete_any_collection); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
fn invite_edit_custom_empty_permissions_persist_zero_mask() { |
|
|
|
|
|
let payload = HashMap::new(); |
|
|
|
|
|
|
|
|
|
|
|
let (access_all, permission_mask) = |
|
|
|
|
|
resolve_membership_custom_permissions("Custom", MembershipType::Manager as i32, &payload).unwrap(); |
|
|
|
|
|
|
|
|
|
|
|
assert!(!access_all); |
|
|
|
|
|
assert_eq!(permission_mask, Some(0)); |
|
|
|
|
|
|
|
|
|
|
|
let parsed = OrganizationUserPermissions::from_mask(permission_mask.unwrap()); |
|
|
|
|
|
assert_eq!(parsed, OrganizationUserPermissions::default()); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
#[derive(Deserialize)] |
|
|
#[derive(Deserialize)] |
|
|
#[serde(rename_all = "camelCase")] |
|
|
#[serde(rename_all = "camelCase")] |
|
|
struct OrgData { |
|
|
struct OrgData { |
|
|
@ -1043,7 +1194,7 @@ impl InviteData { |
|
|
async fn send_invite( |
|
|
async fn send_invite( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<InviteData>, |
|
|
data: Json<InviteData>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -1066,14 +1217,7 @@ async fn send_invite( |
|
|
err!("Only Owners can invite Managers, Admins or Owners") |
|
|
err!("Only Owners can invite Managers, Admins or Owners") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
|
|
|
let (access_all, permissions_mask) = resolve_membership_custom_permissions(raw_type, new_type, &data.permissions)?; |
|
|
// Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
|
|
|
|
|
|
// If the box is not checked, the user will still be a manager, but not with the access_all permission
|
|
|
|
|
|
let access_all = new_type >= MembershipType::Admin |
|
|
|
|
|
|| (raw_type.eq("4") |
|
|
|
|
|
&& data.permissions.get("editAnyCollection") == Some(&json!(true)) |
|
|
|
|
|
&& data.permissions.get("deleteAnyCollection") == Some(&json!(true)) |
|
|
|
|
|
&& data.permissions.get("createNewCollections") == Some(&json!(true))); |
|
|
|
|
|
|
|
|
|
|
|
let mut user_created: bool = false; |
|
|
let mut user_created: bool = false; |
|
|
for email in &data.emails { |
|
|
for email in &data.emails { |
|
|
@ -1118,6 +1262,7 @@ async fn send_invite( |
|
|
new_member.access_all = access_all; |
|
|
new_member.access_all = access_all; |
|
|
new_member.atype = new_type; |
|
|
new_member.atype = new_type; |
|
|
new_member.status = member_status; |
|
|
new_member.status = member_status; |
|
|
|
|
|
new_member.permissions = permissions_mask; |
|
|
new_member.save(&conn).await?; |
|
|
new_member.save(&conn).await?; |
|
|
|
|
|
|
|
|
if CONFIG.mail_enabled() { |
|
|
if CONFIG.mail_enabled() { |
|
|
@ -1194,7 +1339,7 @@ async fn send_invite( |
|
|
async fn bulk_reinvite_members( |
|
|
async fn bulk_reinvite_members( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkMembershipIds>, |
|
|
data: Json<BulkMembershipIds>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -1229,7 +1374,7 @@ async fn bulk_reinvite_members( |
|
|
async fn reinvite_member( |
|
|
async fn reinvite_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -1360,7 +1505,7 @@ struct BulkConfirmData { |
|
|
async fn bulk_confirm_invite( |
|
|
async fn bulk_confirm_invite( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkConfirmData>, |
|
|
data: Json<BulkConfirmData>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
@ -1404,7 +1549,7 @@ async fn confirm_invite( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
data: Json<ConfirmData>, |
|
|
data: Json<ConfirmData>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
@ -1417,7 +1562,7 @@ async fn confirm_invite_impl( |
|
|
org_id: &OrganizationId, |
|
|
org_id: &OrganizationId, |
|
|
member_id: &MembershipId, |
|
|
member_id: &MembershipId, |
|
|
key: &str, |
|
|
key: &str, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageUsersHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
nt: &Notify<'_>, |
|
|
nt: &Notify<'_>, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
@ -1502,7 +1647,7 @@ async fn get_user( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
data: GetOrgUserData, |
|
|
data: GetOrgUserData, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -1533,7 +1678,7 @@ async fn put_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
data: Json<EditUserData>, |
|
|
data: Json<EditUserData>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
edit_member(org_id, member_id, data, headers, conn).await |
|
|
edit_member(org_id, member_id, data, headers, conn).await |
|
|
@ -1544,7 +1689,7 @@ async fn edit_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
data: Json<EditUserData>, |
|
|
data: Json<EditUserData>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -1560,14 +1705,8 @@ async fn edit_member( |
|
|
err!("Invalid type") |
|
|
err!("Invalid type") |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
|
|
|
let (access_all, permissions_mask) = |
|
|
// Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
|
|
|
resolve_membership_custom_permissions(raw_type, new_type as i32, &data.permissions)?; |
|
|
// If the box is not checked, the user will still be a manager, but not with the access_all permission
|
|
|
|
|
|
let access_all = new_type >= MembershipType::Admin |
|
|
|
|
|
|| (raw_type.eq("4") |
|
|
|
|
|
&& data.permissions.get("editAnyCollection") == Some(&json!(true)) |
|
|
|
|
|
&& data.permissions.get("deleteAnyCollection") == Some(&json!(true)) |
|
|
|
|
|
&& data.permissions.get("createNewCollections") == Some(&json!(true))); |
|
|
|
|
|
|
|
|
|
|
|
let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else { |
|
|
let Some(mut member_to_edit) = Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await else { |
|
|
err!("The specified user isn't member of the organization") |
|
|
err!("The specified user isn't member of the organization") |
|
|
@ -1596,6 +1735,7 @@ async fn edit_member( |
|
|
|
|
|
|
|
|
member_to_edit.access_all = access_all; |
|
|
member_to_edit.access_all = access_all; |
|
|
member_to_edit.atype = new_type as i32; |
|
|
member_to_edit.atype = new_type as i32; |
|
|
|
|
|
member_to_edit.permissions = permissions_mask; |
|
|
|
|
|
|
|
|
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
|
|
|
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
|
|
|
// We need to perform the check after changing the type since `admin` is exempt.
|
|
|
// We need to perform the check after changing the type since `admin` is exempt.
|
|
|
@ -1654,7 +1794,7 @@ async fn edit_member( |
|
|
async fn bulk_delete_member( |
|
|
async fn bulk_delete_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkMembershipIds>, |
|
|
data: Json<BulkMembershipIds>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
@ -1690,7 +1830,7 @@ async fn bulk_delete_member( |
|
|
async fn delete_member( |
|
|
async fn delete_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
@ -1700,7 +1840,7 @@ async fn delete_member( |
|
|
async fn delete_member_impl( |
|
|
async fn delete_member_impl( |
|
|
org_id: &OrganizationId, |
|
|
org_id: &OrganizationId, |
|
|
member_id: &MembershipId, |
|
|
member_id: &MembershipId, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageUsersHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
nt: &Notify<'_>, |
|
|
nt: &Notify<'_>, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
@ -1754,7 +1894,7 @@ async fn delete_member_impl( |
|
|
async fn bulk_public_keys( |
|
|
async fn bulk_public_keys( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkMembershipIds>, |
|
|
data: Json<BulkMembershipIds>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -1966,7 +2106,7 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[get("/organizations/<org_id>/policies")] |
|
|
#[get("/organizations/<org_id>/policies")] |
|
|
async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult { |
|
|
async fn list_policies(org_id: OrganizationId, headers: ManagePoliciesHeaders, conn: DbConn) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
} |
|
|
} |
|
|
@ -2032,7 +2172,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: OrgMemberH |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[get("/organizations/<org_id>/policies/<pol_type>", rank = 3)] |
|
|
#[get("/organizations/<org_id>/policies/<pol_type>", rank = 3)] |
|
|
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, conn: DbConn) -> JsonResult { |
|
|
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: ManagePoliciesHeaders, conn: DbConn) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
} |
|
|
} |
|
|
@ -2069,7 +2209,7 @@ async fn put_policy( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
pol_type: i32, |
|
|
pol_type: i32, |
|
|
data: Json<PutPolicy>, |
|
|
data: Json<PutPolicy>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManagePoliciesHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2189,7 +2329,7 @@ async fn put_policy_vnext( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
pol_type: i32, |
|
|
pol_type: i32, |
|
|
data: Json<PutPolicy>, |
|
|
data: Json<PutPolicy>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManagePoliciesHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
put_policy(org_id, pol_type, data, headers, conn).await |
|
|
put_policy(org_id, pol_type, data, headers, conn).await |
|
|
@ -2266,7 +2406,7 @@ struct BulkRevokeMembershipIds { |
|
|
async fn revoke_member( |
|
|
async fn revoke_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
revoke_member_impl(&org_id, &member_id, &headers, &conn).await |
|
|
revoke_member_impl(&org_id, &member_id, &headers, &conn).await |
|
|
@ -2276,7 +2416,7 @@ async fn revoke_member( |
|
|
async fn bulk_revoke_members( |
|
|
async fn bulk_revoke_members( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkRevokeMembershipIds>, |
|
|
data: Json<BulkRevokeMembershipIds>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2315,7 +2455,7 @@ async fn bulk_revoke_members( |
|
|
async fn revoke_member_impl( |
|
|
async fn revoke_member_impl( |
|
|
org_id: &OrganizationId, |
|
|
org_id: &OrganizationId, |
|
|
member_id: &MembershipId, |
|
|
member_id: &MembershipId, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageUsersHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != &headers.org_id { |
|
|
if org_id != &headers.org_id { |
|
|
@ -2329,6 +2469,9 @@ async fn revoke_member_impl( |
|
|
if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { |
|
|
if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { |
|
|
err!("Only owners can revoke other owners") |
|
|
err!("Only owners can revoke other owners") |
|
|
} |
|
|
} |
|
|
|
|
|
if !headers.is_admin_or_owner && member.atype != MembershipType::User { |
|
|
|
|
|
err!("Custom roles can only revoke users") |
|
|
|
|
|
} |
|
|
if member.atype == MembershipType::Owner |
|
|
if member.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 |
|
|
{ |
|
|
{ |
|
|
@ -2359,7 +2502,7 @@ async fn revoke_member_impl( |
|
|
async fn restore_member_vnext( |
|
|
async fn restore_member_vnext( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
// Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy.
|
|
|
// Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy.
|
|
|
@ -2371,7 +2514,7 @@ async fn restore_member_vnext( |
|
|
async fn restore_member( |
|
|
async fn restore_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
restore_member_impl(&org_id, &member_id, &headers, &conn).await |
|
|
restore_member_impl(&org_id, &member_id, &headers, &conn).await |
|
|
@ -2381,7 +2524,7 @@ async fn restore_member( |
|
|
async fn bulk_restore_members( |
|
|
async fn bulk_restore_members( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkMembershipIds>, |
|
|
data: Json<BulkMembershipIds>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageUsersHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2415,7 +2558,7 @@ async fn bulk_restore_members( |
|
|
async fn restore_member_impl( |
|
|
async fn restore_member_impl( |
|
|
org_id: &OrganizationId, |
|
|
org_id: &OrganizationId, |
|
|
member_id: &MembershipId, |
|
|
member_id: &MembershipId, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageUsersHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != &headers.org_id { |
|
|
if org_id != &headers.org_id { |
|
|
@ -2429,6 +2572,9 @@ async fn restore_member_impl( |
|
|
if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { |
|
|
if member.atype == MembershipType::Owner && headers.membership_type != MembershipType::Owner { |
|
|
err!("Only owners can restore other owners") |
|
|
err!("Only owners can restore other owners") |
|
|
} |
|
|
} |
|
|
|
|
|
if !headers.is_admin_or_owner && member.atype != MembershipType::User { |
|
|
|
|
|
err!("Custom roles can only restore users") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
member.restore(); |
|
|
member.restore(); |
|
|
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
|
|
|
// This check is also done at accept_invite, _confirm_invite, _activate_member, edit_member, admin::update_membership_type
|
|
|
@ -2579,7 +2725,7 @@ async fn post_group( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
data: Json<GroupRequest>, |
|
|
data: Json<GroupRequest>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
put_group(org_id, group_id, data, headers, conn).await |
|
|
put_group(org_id, group_id, data, headers, conn).await |
|
|
@ -2588,7 +2734,7 @@ async fn post_group( |
|
|
#[post("/organizations/<org_id>/groups", data = "<data>")] |
|
|
#[post("/organizations/<org_id>/groups", data = "<data>")] |
|
|
async fn post_groups( |
|
|
async fn post_groups( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
data: Json<GroupRequest>, |
|
|
data: Json<GroupRequest>, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
@ -2623,7 +2769,7 @@ async fn put_group( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
data: Json<GroupRequest>, |
|
|
data: Json<GroupRequest>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2664,7 +2810,7 @@ async fn add_update_group( |
|
|
collections: Vec<CollectionData>, |
|
|
collections: Vec<CollectionData>, |
|
|
members: Vec<MembershipId>, |
|
|
members: Vec<MembershipId>, |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageGroupsHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
group.save(conn).await?; |
|
|
group.save(conn).await?; |
|
|
@ -2704,7 +2850,7 @@ async fn add_update_group( |
|
|
async fn get_group_details( |
|
|
async fn get_group_details( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2725,21 +2871,26 @@ async fn get_group_details( |
|
|
async fn post_delete_group( |
|
|
async fn post_delete_group( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
delete_group_impl(&org_id, &group_id, &headers, &conn).await |
|
|
delete_group_impl(&org_id, &group_id, &headers, &conn).await |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[delete("/organizations/<org_id>/groups/<group_id>")] |
|
|
#[delete("/organizations/<org_id>/groups/<group_id>")] |
|
|
async fn delete_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> EmptyResult { |
|
|
async fn delete_group( |
|
|
|
|
|
org_id: OrganizationId, |
|
|
|
|
|
group_id: GroupId, |
|
|
|
|
|
headers: ManageGroupsHeaders, |
|
|
|
|
|
conn: DbConn, |
|
|
|
|
|
) -> EmptyResult { |
|
|
delete_group_impl(&org_id, &group_id, &headers, &conn).await |
|
|
delete_group_impl(&org_id, &group_id, &headers, &conn).await |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async fn delete_group_impl( |
|
|
async fn delete_group_impl( |
|
|
org_id: &OrganizationId, |
|
|
org_id: &OrganizationId, |
|
|
group_id: &GroupId, |
|
|
group_id: &GroupId, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageGroupsHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != &headers.org_id { |
|
|
if org_id != &headers.org_id { |
|
|
@ -2771,7 +2922,7 @@ async fn delete_group_impl( |
|
|
async fn bulk_delete_groups( |
|
|
async fn bulk_delete_groups( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
data: Json<BulkGroupIds>, |
|
|
data: Json<BulkGroupIds>, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2790,7 +2941,12 @@ async fn bulk_delete_groups( |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
#[get("/organizations/<org_id>/groups/<group_id>", rank = 2)] |
|
|
#[get("/organizations/<org_id>/groups/<group_id>", rank = 2)] |
|
|
async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, conn: DbConn) -> JsonResult { |
|
|
async fn get_group( |
|
|
|
|
|
org_id: OrganizationId, |
|
|
|
|
|
group_id: GroupId, |
|
|
|
|
|
headers: ManageGroupsHeaders, |
|
|
|
|
|
conn: DbConn, |
|
|
|
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
} |
|
|
} |
|
|
@ -2809,7 +2965,7 @@ async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHead |
|
|
async fn get_group_members( |
|
|
async fn get_group_members( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2836,7 +2992,7 @@ async fn get_group_members( |
|
|
async fn put_group_members( |
|
|
async fn put_group_members( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
data: Json<Vec<MembershipId>>, |
|
|
data: Json<Vec<MembershipId>>, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
@ -2884,7 +3040,7 @@ async fn post_delete_group_member( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
group_id: GroupId, |
|
|
group_id: GroupId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageGroupsHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -2967,7 +3123,7 @@ async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders |
|
|
async fn put_recover_account( |
|
|
async fn put_recover_account( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageResetPasswordHeaders, |
|
|
data: Json<OrganizationUserRecoverAccountRequest>, |
|
|
data: Json<OrganizationUserRecoverAccountRequest>, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
@ -2985,7 +3141,7 @@ async fn put_recover_account( |
|
|
async fn put_reset_password( |
|
|
async fn put_reset_password( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageResetPasswordHeaders, |
|
|
data: Json<OrganizationUserRecoverAccountRequest>, |
|
|
data: Json<OrganizationUserRecoverAccountRequest>, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
@ -2996,7 +3152,7 @@ async fn put_reset_password( |
|
|
async fn recover_account( |
|
|
async fn recover_account( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageResetPasswordHeaders, |
|
|
reset_request: OrganizationUserRecoverAccountRequest, |
|
|
reset_request: OrganizationUserRecoverAccountRequest, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
nt: Notify<'_>, |
|
|
nt: Notify<'_>, |
|
|
@ -3056,7 +3212,7 @@ async fn recover_account( |
|
|
async fn get_reset_password_details( |
|
|
async fn get_reset_password_details( |
|
|
org_id: OrganizationId, |
|
|
org_id: OrganizationId, |
|
|
member_id: MembershipId, |
|
|
member_id: MembershipId, |
|
|
headers: AdminHeaders, |
|
|
headers: ManageResetPasswordHeaders, |
|
|
conn: DbConn, |
|
|
conn: DbConn, |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
@ -3092,7 +3248,7 @@ async fn get_reset_password_details( |
|
|
async fn check_reset_password_applicable_and_permissions( |
|
|
async fn check_reset_password_applicable_and_permissions( |
|
|
org_id: &OrganizationId, |
|
|
org_id: &OrganizationId, |
|
|
member_id: &MembershipId, |
|
|
member_id: &MembershipId, |
|
|
headers: &AdminHeaders, |
|
|
headers: &ManageResetPasswordHeaders, |
|
|
conn: &DbConn, |
|
|
conn: &DbConn, |
|
|
) -> EmptyResult { |
|
|
) -> EmptyResult { |
|
|
check_reset_password_applicable(org_id, conn).await?; |
|
|
check_reset_password_applicable(org_id, conn).await?; |
|
|
@ -3105,6 +3261,7 @@ async fn check_reset_password_applicable_and_permissions( |
|
|
match headers.membership_type { |
|
|
match headers.membership_type { |
|
|
MembershipType::Owner => Ok(()), |
|
|
MembershipType::Owner => Ok(()), |
|
|
MembershipType::Admin if target_user.atype <= MembershipType::Admin => Ok(()), |
|
|
MembershipType::Admin if target_user.atype <= MembershipType::Admin => Ok(()), |
|
|
|
|
|
MembershipType::Manager if !headers.is_admin_or_owner && target_user.atype == MembershipType::User => Ok(()), |
|
|
_ => err!("No permission to reset this user's password"), |
|
|
_ => err!("No permission to reset this user's password"), |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -3184,7 +3341,7 @@ async fn put_reset_password_enrollment( |
|
|
// Vaultwarden does not yet support exporting only managed collections!
|
|
|
// Vaultwarden does not yet support exporting only managed collections!
|
|
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52
|
|
|
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/OrganizationExportController.cs#L52
|
|
|
#[get("/organizations/<org_id>/export")] |
|
|
#[get("/organizations/<org_id>/export")] |
|
|
async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, conn: DbConn) -> JsonResult { |
|
|
async fn get_org_export(org_id: OrganizationId, headers: AccessImportExportHeaders, conn: DbConn) -> JsonResult { |
|
|
if org_id != headers.org_id { |
|
|
if org_id != headers.org_id { |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
} |
|
|
} |
|
|
|