Browse Source

Fix delete-only collection access in web vault

pull/7397/head
tom27052006 6 days ago
parent
commit
e67fb30c5f
  1. 72
      src/api/core/organizations.rs

72
src/api/core/organizations.rs

@ -51,6 +51,7 @@ pub fn routes() -> Vec<Route> {
post_organization_collection_delete,
bulk_delete_organization_collections,
post_bulk_collections,
get_assigned_org_details,
get_org_details,
get_org_domain_sso_verified,
get_members,
@ -917,6 +918,49 @@ struct OrgIdData {
organization_id: OrganizationId,
}
fn filter_ciphers_for_organization(ciphers: Vec<Cipher>, org_id: &OrganizationId) -> Vec<Cipher> {
ciphers.into_iter().filter(|cipher| cipher.organization_uuid.as_ref() == Some(org_id)).collect()
}
// The Admin Console uses this endpoint when the acting member is not allowed to read every
// cipher in the organization. In particular, a Custom member with only DeleteAnyCollection
// needs an empty successful response so the collection list can finish loading and expose its
// collection-only delete controls.
//
// Security: start from the regular user-visible cipher query and then constrain the result to
// the requested organization. DeleteAnyCollection itself must never make cipher contents visible.
#[get("/ciphers/organization-details/assigned?<data..>")]
async fn get_assigned_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult {
if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &conn).await.is_none() {
err_code!(
"Resource not found.",
"User is not a confirmed member of the organization",
rocket::http::Status::NotFound.code
);
}
let ciphers = filter_ciphers_for_organization(
Cipher::find_by_user_visible(&headers.user.uuid, &conn).await,
&data.organization_id,
);
let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;
let mut ciphers_json = Vec::with_capacity(ciphers.len());
for cipher in ciphers {
ciphers_json.push(
cipher
.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &conn)
.await?,
);
}
Ok(Json(json!({
"data": ciphers_json,
"object": "list",
"continuationToken": null,
})))
}
#[get("/ciphers/organization-details?<data..>")]
async fn get_org_details(data: OrgIdData, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult {
if data.organization_id != headers.membership.org_uuid {
@ -3604,8 +3648,32 @@ mod tests {
use serde_json::{Value, json};
use super::{CustomRolePermissions, may_change_group_membership, may_change_member_type};
use crate::db::models::{Membership, MembershipStatus, MembershipType};
use super::{
CustomRolePermissions, filter_ciphers_for_organization, may_change_group_membership, may_change_member_type,
};
use crate::db::models::{Cipher, Membership, MembershipStatus, MembershipType, OrganizationId};
#[test]
fn assigned_cipher_response_is_scoped_to_requested_organization() {
let requested_org: OrganizationId = "requested-org".to_owned().into();
let other_org: OrganizationId = "other-org".to_owned().into();
let mut requested_cipher = Cipher::new(1, "requested".to_owned());
requested_cipher.organization_uuid = Some(requested_org.clone());
let requested_cipher_id = requested_cipher.uuid.clone();
let mut other_cipher = Cipher::new(1, "other".to_owned());
other_cipher.organization_uuid = Some(other_org);
let personal_cipher = Cipher::new(1, "personal".to_owned());
let filtered =
filter_ciphers_for_organization(vec![other_cipher, personal_cipher, requested_cipher], &requested_org);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].uuid, requested_cipher_id);
assert_eq!(filtered[0].organization_uuid.as_ref(), Some(&requested_org));
}
#[test]
fn manage_users_caller_cannot_change_member_role() {

Loading…
Cancel
Save