From 983c862d22249b301e760b074072cd01222aff0e Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 24 Jan 2025 18:39:59 +0100 Subject: [PATCH] Add more org_id checks Added more org_id checks at all functions which use the org_id in there path. Signed-off-by: BlackDex --- src/api/core/events.rs | 11 +- src/api/core/organizations.rs | 215 +++++++++++++++++++++++++--------- src/auth.rs | 5 + 3 files changed, 176 insertions(+), 55 deletions(-) diff --git a/src/api/core/events.rs b/src/api/core/events.rs index 11ae66c7..012f46cc 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -34,9 +34,13 @@ struct EventRange { async fn get_org_events( org_id: OrganizationId, data: EventRange, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } + // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { @@ -100,9 +104,12 @@ async fn get_user_events( org_id: OrganizationId, member_id: MembershipId, data: EventRange, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 83f21618..c610f5b5 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -338,12 +338,15 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json } #[get("/organizations//collections")] -async fn get_org_collections(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { - Json(json!({ +async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } + Ok(Json(json!({ "data": _get_org_collections(&org_id, &mut conn).await, "object": "list", "continuationToken": null, - })) + }))) } #[get("/organizations//collections/details")] @@ -352,6 +355,9 @@ async fn get_org_collections_details( headers: ManagerHeadersLoose, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let mut data = Vec::new(); let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else { @@ -426,6 +432,9 @@ async fn post_organization_collections( data: Json, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let data: NewCollectionData = data.into_inner(); let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { @@ -498,6 +507,9 @@ async fn post_organization_collection_update( data: Json, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: NewCollectionData = data.into_inner(); if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() { @@ -558,9 +570,12 @@ async fn delete_organization_collection_member( org_id: OrganizationId, col_id: CollectionId, member_id: MembershipId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found", "Collection does not exist or does not belong to this organization") }; @@ -651,6 +666,9 @@ async fn bulk_delete_organization_collections( data: Json, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkCollectionIds = data.into_inner(); let collections = data.ids; @@ -670,6 +688,9 @@ async fn get_org_collection_detail( headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &mut conn).await { None => err!("Collection not found"), Some(collection) => { @@ -731,9 +752,12 @@ async fn get_org_collection_detail( async fn get_collection_users( org_id: OrganizationId, col_id: CollectionId, - _headers: ManagerHeaders, + headers: ManagerHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Get org and collection, check that collection is from org let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else { err!("Collection not found in Organization") @@ -757,9 +781,12 @@ async fn put_collection_users( org_id: OrganizationId, col_id: CollectionId, data: Json>, - _headers: ManagerHeaders, + headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Get org and collection, check that collection is from org if Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await.is_none() { err!("Collection not found in Organization") @@ -826,9 +853,12 @@ struct GetOrgUserData { async fn get_members( data: GetOrgUserData, org_id: OrganizationId, - _headers: ManagerHeadersLoose, + headers: ManagerHeadersLoose, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let mut users_json = Vec::new(); for u in Membership::find_by_org(&org_id, &mut conn).await { users_json.push( @@ -841,20 +871,23 @@ async fn get_members( ); } - Json(json!({ + Ok(Json(json!({ "data": users_json, "object": "list", "continuationToken": null, - })) + }))) } #[post("/organizations//keys", data = "")] async fn post_org_keys( org_id: OrganizationId, data: Json, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: OrgKeyData = data.into_inner(); let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await { @@ -917,6 +950,9 @@ async fn send_invite( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let mut data: InviteData = data.into_inner(); // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission @@ -1057,7 +1093,10 @@ async fn bulk_reinvite_members( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1076,11 +1115,11 @@ async fn bulk_reinvite_members( )) } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[post("/organizations//users//reinvite")] @@ -1249,7 +1288,10 @@ async fn bulk_confirm_invite( headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1275,11 +1317,11 @@ async fn bulk_confirm_invite( None => error!("No keys to confirm"), } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[post("/organizations//users//confirm", data = "")] @@ -1376,19 +1418,22 @@ async fn _confirm_invite( #[get("/organizations//users/mini-details", rank = 1)] async fn get_org_user_mini_details( org_id: OrganizationId, - _headers: ManagerHeadersLoose, + headers: ManagerHeadersLoose, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let mut members_json = Vec::new(); for m in Membership::find_by_org(&org_id, &mut conn).await { members_json.push(m.to_json_mini_details(&mut conn).await); } - Json(json!({ + Ok(Json(json!({ "data": members_json, "object": "list", "continuationToken": null, - })) + }))) } #[get("/organizations//users/?", rank = 2)] @@ -1396,9 +1441,12 @@ async fn get_user( org_id: OrganizationId, member_id: MembershipId, data: GetOrgUserData, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else { err!("The specified user isn't a member of the organization") }; @@ -1442,6 +1490,9 @@ async fn edit_member( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let mut data: EditUserData = data.into_inner(); // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission @@ -1563,7 +1614,10 @@ async fn bulk_delete_member( headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1582,11 +1636,11 @@ async fn bulk_delete_member( )) } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[delete("/organizations//users/")] @@ -1656,9 +1710,12 @@ async fn _delete_member( async fn bulk_public_keys( org_id: OrganizationId, data: Json, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: BulkMembershipIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1682,11 +1739,11 @@ async fn bulk_public_keys( } } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } use super::ciphers::update_cipher_from_data; @@ -1831,15 +1888,18 @@ async fn post_bulk_collections(data: Json, headers: Headers } #[get("/organizations//policies")] -async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn: DbConn) -> Json { +async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await; let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); - Json(json!({ + Ok(Json(json!({ "data": policies_json, "object": "list", "continuationToken": null - })) + }))) } #[get("/organizations//policies/token?")] @@ -1867,7 +1927,10 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbCo } #[get("/organizations//policies/")] -async fn get_policy(org_id: OrganizationId, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { err!("Invalid or unsupported policy type") }; @@ -1896,6 +1959,9 @@ async fn put_policy( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data: PolicyData = data.into_inner(); let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else { @@ -2229,7 +2295,7 @@ async fn bulk_deactivate_members( data: Json, headers: AdminHeaders, conn: DbConn, -) -> Json { +) -> JsonResult { bulk_revoke_members(org_id, data, headers, conn).await } @@ -2249,7 +2315,10 @@ async fn bulk_revoke_members( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -2273,11 +2342,11 @@ async fn bulk_revoke_members( None => error!("No users to revoke"), } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } async fn _revoke_member( @@ -2338,7 +2407,7 @@ async fn bulk_activate_members( data: Json, headers: AdminHeaders, conn: DbConn, -) -> Json { +) -> JsonResult { bulk_restore_members(org_id, data, headers, conn).await } @@ -2358,7 +2427,10 @@ async fn bulk_restore_members( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -2377,11 +2449,11 @@ async fn bulk_restore_members( )); } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } async fn _restore_member( @@ -2438,7 +2510,10 @@ async fn _restore_member( } #[get("/organizations//groups")] -async fn get_groups(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { +async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { + if org_id != headers.membership.org_uuid { + err!("Organization not found", "Organization id's do not match"); + } let groups: Vec = if CONFIG.org_groups_enabled() { // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() let groups = Group::find_by_organization(&org_id, &mut conn).await; @@ -2626,9 +2701,12 @@ async fn add_update_group( async fn get_group_details( org_id: OrganizationId, group_id: GroupId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2708,7 +2786,10 @@ async fn bulk_delete_groups( } #[get("/organizations//groups/", rank = 2)] -async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { +async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2724,9 +2805,12 @@ async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHea async fn get_group_members( org_id: OrganizationId, group_id: GroupId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2786,9 +2870,12 @@ async fn put_group_members( async fn get_user_groups( org_id: OrganizationId, member_id: MembershipId, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2828,6 +2915,9 @@ async fn put_user_groups( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2877,6 +2967,9 @@ async fn delete_group_member( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2922,7 +3015,14 @@ struct OrganizationUserResetPasswordRequest { // But the clients do not seem to use this at all // Just add it here in case they will #[get("/organizations//public-key")] -async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_organization_public_key( + org_id: OrganizationId, + headers: OrgMemberHeaders, + mut conn: DbConn, +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Organization not found") }; @@ -2936,7 +3036,7 @@ async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, // Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients // https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468 #[get("/organizations//keys")] -async fn get_organization_keys(org_id: OrganizationId, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult { get_organization_public_key(org_id, headers, conn).await } @@ -2949,6 +3049,9 @@ async fn put_reset_password( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Required organization not found") }; @@ -3005,6 +3108,9 @@ async fn get_reset_password_details( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { err!("Required organization not found") }; @@ -3126,7 +3232,10 @@ async fn get_org_export( headers: AdminHeaders, client_version: Option, mut conn: DbConn, -) -> Json { +) -> JsonResult { + if org_id != headers.org_id { + err!("Organization not found", "Organization id's do not match"); + } // Since version v2023.1.0 the format of the export is different. // Also, this endpoint was created since v2022.9.0. // Therefore, we will check for any version smaller then v2023.1.0 and return a different response. @@ -3142,7 +3251,7 @@ async fn get_org_export( // Also both main keys here need to be lowercase, else the export will fail. if use_list_response_model { // Backwards compatible pre v2023.1.0 response - Json(json!({ + Ok(Json(json!({ "collections": { "data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "object": "list", @@ -3153,13 +3262,13 @@ async fn get_org_export( "object": "list", "continuationToken": null, } - })) + }))) } else { // v2023.1.0 and newer response - Json(json!({ + Ok(Json(json!({ "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), - })) + }))) } } diff --git a/src/auth.rs b/src/auth.rs index 92913a67..e4827c80 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -612,6 +612,7 @@ pub struct AdminHeaders { pub user: User, pub membership_type: MembershipType, pub ip: ClientIp, + pub org_id: OrganizationId, } #[rocket::async_trait] @@ -627,6 +628,7 @@ impl<'r> FromRequest<'r> for AdminHeaders { user: headers.user, membership_type: headers.membership_type, ip: headers.ip, + org_id: headers.membership.org_uuid, }) } else { err_handler!("You need to be Admin or Owner to call this endpoint") @@ -672,6 +674,7 @@ pub struct ManagerHeaders { pub device: Device, pub user: User, pub ip: ClientIp, + pub org_id: OrganizationId, } #[rocket::async_trait] @@ -700,6 +703,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { device: headers.device, user: headers.user, ip: headers.ip, + org_id: headers.membership.org_uuid, }) } else { err_handler!("You need to be a Manager, Admin or Owner to call this endpoint") @@ -779,6 +783,7 @@ impl ManagerHeaders { device: h.device, user: h.user, ip: h.ip, + org_id: h.membership.org_uuid, }) } }