mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-04-03 07:39:20 -07:00
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:
committed by
GitHub
parent
f62a7a66c8
commit
787822854c
@@ -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;
|
||||
|
||||
member.delete(&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;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user