|
|
|
@ -131,6 +131,24 @@ struct FullCollectionData { |
|
|
|
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)] |
|
|
|
#[serde(rename_all = "camelCase")] |
|
|
|
struct CollectionGroupData { |
|
|
|
@ -233,30 +251,30 @@ async fn post_delete_organization( |
|
|
|
} |
|
|
|
|
|
|
|
#[post("/organizations/<org_id>/leave")] |
|
|
|
async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbConn) -> EmptyResult { |
|
|
|
match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { |
|
|
|
None => err!("User not part of organization"), |
|
|
|
Some(member) => { |
|
|
|
if member.atype == MembershipType::Owner |
|
|
|
&& Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 |
|
|
|
{ |
|
|
|
err!("The last owner can't leave") |
|
|
|
} |
|
|
|
|
|
|
|
log_event( |
|
|
|
EventType::OrganizationUserLeft as i32, |
|
|
|
&member.uuid, |
|
|
|
&org_id, |
|
|
|
&headers.user.uuid, |
|
|
|
headers.device.atype, |
|
|
|
&headers.ip.ip, |
|
|
|
&conn, |
|
|
|
) |
|
|
|
.await; |
|
|
|
async fn leave_organization(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> EmptyResult { |
|
|
|
if headers.membership.status != MembershipStatus::Confirmed as i32 { |
|
|
|
err!("You need to be a Member of the Organization to call this endpoint") |
|
|
|
} |
|
|
|
let membership = headers.membership; |
|
|
|
|
|
|
|
member.delete(&conn).await |
|
|
|
} |
|
|
|
if membership.atype == MembershipType::Owner |
|
|
|
&& Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &conn).await <= 1 |
|
|
|
{ |
|
|
|
err!("The last owner can't leave") |
|
|
|
} |
|
|
|
|
|
|
|
log_event( |
|
|
|
EventType::OrganizationUserLeft as i32, |
|
|
|
&membership.uuid, |
|
|
|
&org_id, |
|
|
|
&headers.user.uuid, |
|
|
|
headers.device.atype, |
|
|
|
&headers.ip.ip, |
|
|
|
&conn, |
|
|
|
) |
|
|
|
.await; |
|
|
|
|
|
|
|
membership.delete(&conn).await |
|
|
|
} |
|
|
|
|
|
|
|
#[get("/organizations/<org_id>")] |
|
|
|
@ -480,12 +498,9 @@ async fn post_organization_collections( |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
let data: FullCollectionData = data.into_inner(); |
|
|
|
data.validate(&org_id, &conn).await?; |
|
|
|
|
|
|
|
let Some(org) = Organization::find_by_uuid(&org_id, &conn).await else { |
|
|
|
err!("Can't find organization details") |
|
|
|
}; |
|
|
|
|
|
|
|
let collection = Collection::new(org.uuid, data.name, data.external_id); |
|
|
|
let collection = Collection::new(org_id.clone(), data.name, data.external_id); |
|
|
|
collection.save(&conn).await?; |
|
|
|
|
|
|
|
log_event( |
|
|
|
@ -501,7 +516,7 @@ async fn post_organization_collections( |
|
|
|
|
|
|
|
for group in data.groups { |
|
|
|
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage) |
|
|
|
.save(&conn) |
|
|
|
.save(&org_id, &conn) |
|
|
|
.await?; |
|
|
|
} |
|
|
|
|
|
|
|
@ -579,10 +594,10 @@ async fn post_bulk_access_collections( |
|
|
|
) |
|
|
|
.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 { |
|
|
|
CollectionGroup::new(col_id.clone(), group.id.clone(), group.read_only, group.hide_passwords, group.manage) |
|
|
|
.save(&conn) |
|
|
|
.save(&org_id, &conn) |
|
|
|
.await?; |
|
|
|
} |
|
|
|
|
|
|
|
@ -627,6 +642,7 @@ async fn post_organization_collection_update( |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
let data: FullCollectionData = data.into_inner(); |
|
|
|
data.validate(&org_id, &conn).await?; |
|
|
|
|
|
|
|
if Organization::find_by_uuid(&org_id, &conn).await.is_none() { |
|
|
|
err!("Can't find organization details") |
|
|
|
@ -655,11 +671,11 @@ async fn post_organization_collection_update( |
|
|
|
) |
|
|
|
.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 { |
|
|
|
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage) |
|
|
|
.save(&conn) |
|
|
|
.save(&org_id, &conn) |
|
|
|
.await?; |
|
|
|
} |
|
|
|
|
|
|
|
@ -1003,6 +1019,24 @@ struct InviteData { |
|
|
|
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>")] |
|
|
|
async fn send_invite( |
|
|
|
org_id: OrganizationId, |
|
|
|
@ -1014,6 +1048,7 @@ async fn send_invite( |
|
|
|
err!("Organization not found", "Organization id's do not match"); |
|
|
|
} |
|
|
|
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
|
|
|
|
// 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
|
|
|
|
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") |
|
|
|
}; |
|
|
|
|
|
|
|
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 => data.reset_password_key, |
|
|
|
false => None, |
|
|
|
}; |
|
|
|
|
|
|
|
// 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() { |
|
|
|
// User was invited from /admin, so they are automatically confirmed
|
|
|
|
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("createNewCollections") == Some(&json!(true))); |
|
|
|
|
|
|
|
let mut member_to_edit = match Membership::find_by_uuid_and_org(&member_id, &org_id, &conn).await { |
|
|
|
Some(member) => member, |
|
|
|
None => err!("The specified user isn't member of the organization"), |
|
|
|
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") |
|
|
|
}; |
|
|
|
|
|
|
|
if new_type != member_to_edit.atype |
|
|
|
@ -1839,7 +1873,6 @@ async fn post_org_import( |
|
|
|
|
|
|
|
#[derive(Deserialize)] |
|
|
|
#[serde(rename_all = "camelCase")] |
|
|
|
#[allow(dead_code)] |
|
|
|
struct BulkCollectionsData { |
|
|
|
organization_id: OrganizationId, |
|
|
|
cipher_ids: Vec<CipherId>, |
|
|
|
@ -1853,6 +1886,10 @@ struct BulkCollectionsData { |
|
|
|
async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers, conn: DbConn) -> EmptyResult { |
|
|
|
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
|
|
|
|
// Also filter based upon the provided collections
|
|
|
|
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.
|
|
|
|
// Return the org policy if it exists, otherwise use the default one.
|
|
|
|
#[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 = |
|
|
|
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() { |
|
|
|
@ -2149,13 +2186,13 @@ fn get_plans() -> Json<Value> { |
|
|
|
} |
|
|
|
|
|
|
|
#[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.
|
|
|
|
Json(_empty_data_json()) |
|
|
|
} |
|
|
|
|
|
|
|
#[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!({ |
|
|
|
"freeTrial":null, |
|
|
|
"inactiveSubscription":null, |
|
|
|
@ -2427,6 +2464,23 @@ impl GroupRequest { |
|
|
|
|
|
|
|
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)] |
|
|
|
@ -2470,6 +2524,8 @@ async fn post_groups( |
|
|
|
} |
|
|
|
|
|
|
|
let group_request = data.into_inner(); |
|
|
|
group_request.validate(&org_id, &conn).await?; |
|
|
|
|
|
|
|
let group = group_request.to_group(&org_id); |
|
|
|
|
|
|
|
log_event( |
|
|
|
@ -2506,10 +2562,12 @@ async fn put_group( |
|
|
|
}; |
|
|
|
|
|
|
|
let group_request = data.into_inner(); |
|
|
|
group_request.validate(&org_id, &conn).await?; |
|
|
|
|
|
|
|
let updated_group = group_request.update_group(group); |
|
|
|
|
|
|
|
CollectionGroup::delete_all_by_group(&group_id, &conn).await?; |
|
|
|
GroupUser::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, &org_id, &conn).await?; |
|
|
|
|
|
|
|
log_event( |
|
|
|
EventType::GroupUpdated as i32, |
|
|
|
@ -2537,7 +2595,7 @@ async fn add_update_group( |
|
|
|
|
|
|
|
for col_selection in collections { |
|
|
|
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 { |
|
|
|
@ -2630,7 +2688,7 @@ async fn _delete_group( |
|
|
|
) |
|
|
|
.await; |
|
|
|
|
|
|
|
group.delete(conn).await |
|
|
|
group.delete(org_id, conn).await |
|
|
|
} |
|
|
|
|
|
|
|
#[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") |
|
|
|
}; |
|
|
|
|
|
|
|
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 |
|
|
|
.iter() |
|
|
|
.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") |
|
|
|
}; |
|
|
|
|
|
|
|
GroupUser::delete_all_by_group(&group_id, &conn).await?; |
|
|
|
|
|
|
|
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 { |
|
|
|
let mut user_entry = GroupUser::new(group_id.clone(), assigned_member.clone()); |
|
|
|
user_entry.save(&conn).await?; |
|
|
|
@ -2951,15 +3015,20 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn) |
|
|
|
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( |
|
|
|
org_id: OrganizationId, |
|
|
|
member_id: MembershipId, |
|
|
|
headers: Headers, |
|
|
|
user_id: UserId, |
|
|
|
headers: OrgMemberHeaders, |
|
|
|
data: Json<OrganizationUserResetPasswordEnrollmentRequest>, |
|
|
|
conn: DbConn, |
|
|
|
) -> 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") |
|
|
|
}; |
|
|
|
|
|
|
|
@ -2986,16 +3055,17 @@ async fn put_reset_password_enrollment( |
|
|
|
.await?; |
|
|
|
} |
|
|
|
|
|
|
|
member.reset_password_key = reset_password_key; |
|
|
|
member.save(&conn).await?; |
|
|
|
membership.reset_password_key = reset_password_key; |
|
|
|
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 |
|
|
|
} else { |
|
|
|
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(()) |
|
|
|
} |
|
|
|
|