/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * * Modified by Coffey Labs in 2026 for INBUXA. */ use crate::utils::server::TestServer; use ::email::mailbox::{INBOX_ID, TRASH_ID}; use jmap_client::{ core::{ error::{MethodError, MethodErrorType}, set::{SetError, SetErrorType}, }, email::{self, Property, import::EmailImportResponse, query::Filter}, mailbox::{self, Role}, principal::ACL, }; use registry::schema::prelude::ObjectType; use serde_json::json; use std::fmt::Debug; use store::ahash::AHashMap; use types::id::Id; pub async fn test(test: &TestServer) { println!("Running ACL tests..."); // Create a group and three test accounts let inbox_id = Id::new(INBOX_ID as u64).to_string(); let trash_id = Id::new(TRASH_ID as u64).to_string(); let admin = test.account("admin@example.com"); let john = test.account("jdoe@example.com"); let jane = test.account("jane.smith@example.com"); let bill = test.account("bill@example.com"); let sales = test.account("sales@example.com"); // Authenticate all accounts let mut john_client = john.jmap_client().await; let mut jane_client = jane.jmap_client().await; let mut bill_client = bill.jmap_client().await; // Insert two emails in each account let mut email_ids = AHashMap::default(); for (client, account_id, name) in [ (&mut john_client, john.id(), "john"), (&mut jane_client, jane.id(), "jane"), (&mut bill_client, bill.id(), "bill"), (&mut admin.jmap_client().await, sales.id(), "sales"), ] { let user_name = client.session().username().to_string(); let mut ids = Vec::with_capacity(2); for (mailbox_id, mailbox_name) in [(&inbox_id, "inbox"), (&trash_id, "trash")] { ids.push( client .set_default_account_id(account_id.to_string()) .email_import( format!( concat!( "From: acl_test@example.com\r\n", "To: {}\r\n", "Subject: Owned by {} in {}\r\n", "\r\n", "This message is owned by {}.", ), user_name, name, mailbox_name, name ) .into_bytes(), [mailbox_id], None::>, None, ) .await .unwrap() .take_id(), ); } email_ids.insert(name, ids); } // John should have access to his emails only assert_eq!( john_client .email_get( email_ids.get("john").unwrap().first().unwrap(), [Property::Subject].into(), ) .await .unwrap() .unwrap() .subject() .unwrap(), "Owned by john in inbox" ); assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_get( email_ids.get("jane").unwrap().first().unwrap(), [Property::Subject].into(), ) .await, ); assert_forbidden( john_client .set_default_account_id(jane.id_string()) .mailbox_get(&inbox_id, None::>) .await, ); assert_forbidden( john_client .set_default_account_id(sales.id_string()) .email_get( email_ids.get("sales").unwrap().first().unwrap(), [Property::Subject].into(), ) .await, ); assert_forbidden( john_client .set_default_account_id(sales.id_string()) .mailbox_get(&inbox_id, None::>) .await, ); assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_query(None::, None::>) .await, ); // Jane grants Inbox ReadItems access to John jane_client .mailbox_update_acl(&inbox_id, john.id_string(), [ACL::ReadItems]) .await .unwrap(); // John should have ReadItems access to Inbox assert_eq!( john_client .set_default_account_id(jane.id_string()) .email_get( email_ids.get("jane").unwrap().first().unwrap(), [Property::Subject].into(), ) .await .unwrap() .unwrap() .subject() .unwrap(), "Owned by jane in inbox" ); assert_eq!( john_client .set_default_account_id(jane.id_string()) .email_query(None::, None::>) .await .unwrap() .ids(), [email_ids.get("jane").unwrap().first().unwrap().as_str()] ); // John's session resource should contain Jane's account details john_client.refresh_session().await.unwrap(); assert_eq!( john_client .session() .account(jane.id_string()) .unwrap() .name(), "jane.smith@example.com" ); // John should not have access to emails in Jane's Trash folder assert!( john_client .set_default_account_id(jane.id_string()) .email_get( email_ids.get("jane").unwrap().last().unwrap(), [Property::Subject].into(), ) .await .unwrap() .is_none() ); // Email/changes must not leak ids of emails in folders John cannot read let jane_inbox_email = email_ids.get("jane").unwrap().first().unwrap().clone(); let jane_trash_email = email_ids.get("jane").unwrap().last().unwrap().clone(); let changed_ids = john_client .set_default_account_id(jane.id_string()) .email_changes("n", None) .await .unwrap() .created() .to_vec(); assert!( changed_ids.contains(&jane_inbox_email), "Email/changes should report the shared Inbox email" ); assert!( !changed_ids.contains(&jane_trash_email), "Email/changes leaked the id of a non-shared Trash email" ); // John should only be able to copy blobs he has access to let blob_id = jane_client .email_get( email_ids.get("jane").unwrap().first().unwrap(), [Property::BlobId].into(), ) .await .unwrap() .unwrap() .take_blob_id(); john_client .set_default_account_id(john.id_string()) .blob_copy(jane.id_string(), &blob_id) .await .unwrap(); let blob_id = jane_client .email_get( email_ids.get("jane").unwrap().last().unwrap(), [Property::BlobId].into(), ) .await .unwrap() .unwrap() .take_blob_id(); assert_forbidden( john_client .set_default_account_id(john.id_string()) .blob_copy(jane.id_string(), &blob_id) .await, ); // John only has ReadItems access to Inbox jane_client .mailbox_update_acl(&inbox_id, john.id_string(), [ACL::ReadItems]) .await .unwrap(); assert_eq!( john_client .set_default_account_id(jane.id_string()) .mailbox_get(&inbox_id, [mailbox::Property::MyRights].into()) .await .unwrap() .unwrap() .my_rights() .unwrap() .acl_list(), vec![ACL::ReadItems] ); // Try to add items using import and copy let blob_id = john_client .set_default_account_id(john.id_string()) .upload( Some(john.id_string()), concat!( "From: acl_test@example.com\r\n", "To: jane.smith@example.com\r\n", "Subject: Created by john in jane's inbox\r\n", "\r\n", "This message is owned by jane.", ) .as_bytes() .to_vec(), None, ) .await .unwrap() .take_blob_id(); let mut request = john_client.set_default_account_id(jane.id_string()).build(); let email_id = request .import_email() .email(&blob_id) .mailbox_ids([&inbox_id]) .create_id(); assert_forbidden( request .send_single::() .await .unwrap() .created(&email_id), ); assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_copy( john.id_string(), email_ids.get("john").unwrap().last().unwrap(), [&inbox_id], None::>, None, ) .await, ); // Grant access and try again jane_client .mailbox_update_acl(&inbox_id, john.id_string(), [ACL::ReadItems, ACL::AddItems]) .await .unwrap(); let mut request = john_client.set_default_account_id(jane.id_string()).build(); let email_id = request .import_email() .email(&blob_id) .mailbox_ids([&inbox_id]) .create_id(); let email_id = request .send_single::() .await .unwrap() .created(&email_id) .unwrap() .take_id(); let email_id_2 = john_client .set_default_account_id(jane.id_string()) .email_copy( john.id_string(), email_ids.get("john").unwrap().last().unwrap(), [&inbox_id], None::>, None, ) .await .unwrap() .take_id(); assert_eq!( jane_client .email_get(&email_id, [Property::Subject].into(),) .await .unwrap() .unwrap() .subject() .unwrap(), "Created by john in jane's inbox" ); assert_eq!( jane_client .email_get(&email_id_2, [Property::Subject].into(),) .await .unwrap() .unwrap() .subject() .unwrap(), "Owned by john in trash" ); // Try removing items assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_destroy(&email_id) .await, ); jane_client .mailbox_update_acl( &inbox_id, john.id_string(), [ACL::ReadItems, ACL::AddItems, ACL::RemoveItems], ) .await .unwrap(); john_client .set_default_account_id(jane.id_string()) .email_destroy(&email_id) .await .unwrap(); // Try to set keywords assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_set_keyword(&email_id_2, "$seen", true) .await, ); jane_client .mailbox_update_acl( &inbox_id, john.id_string(), [ ACL::ReadItems, ACL::AddItems, ACL::RemoveItems, ACL::SetKeywords, ], ) .await .unwrap(); john_client .set_default_account_id(jane.id_string()) .email_set_keyword(&email_id_2, "$seen", true) .await .unwrap(); john_client .set_default_account_id(jane.id_string()) .email_set_keyword(&email_id_2, "my-keyword", true) .await .unwrap(); // Try to create a child assert_forbidden( john_client .set_default_account_id(jane.id_string()) .mailbox_create("John's mailbox", None::<&str>, Role::None) .await, ); jane_client .mailbox_update_acl( &inbox_id, john.id_string(), [ ACL::ReadItems, ACL::AddItems, ACL::RemoveItems, ACL::SetKeywords, ACL::CreateChild, ], ) .await .unwrap(); let mailbox_id = john_client .set_default_account_id(jane.id_string()) .mailbox_create("John's mailbox", Some(&inbox_id), Role::None) .await .unwrap() .take_id(); // Try renaming a mailbox assert_forbidden( john_client .set_default_account_id(jane.id_string()) .mailbox_rename(&mailbox_id, "John's private mailbox") .await, ); jane_client .mailbox_update_acl(&mailbox_id, john.id_string(), [ACL::ReadItems, ACL::Rename]) .await .unwrap(); john_client .set_default_account_id(jane.id_string()) .mailbox_rename(&mailbox_id, "John's private mailbox") .await .unwrap(); // Try moving a message assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_set_mailbox(&email_id_2, &mailbox_id, true) .await, ); jane_client .mailbox_update_acl( &mailbox_id, john.id_string(), [ACL::ReadItems, ACL::Rename, ACL::AddItems], ) .await .unwrap(); john_client .set_default_account_id(jane.id_string()) .email_set_mailbox(&email_id_2, &mailbox_id, true) .await .unwrap(); // Try deleting a mailbox assert_forbidden( john_client .set_default_account_id(jane.id_string()) .mailbox_destroy(&mailbox_id, true) .await, ); jane_client .mailbox_update_acl( &mailbox_id, john.id_string(), [ACL::ReadItems, ACL::Rename, ACL::AddItems, ACL::Delete], ) .await .unwrap(); assert_forbidden( john_client .set_default_account_id(jane.id_string()) .mailbox_destroy(&mailbox_id, true) .await, ); jane_client .mailbox_update_acl( &mailbox_id, john.id_string(), [ ACL::ReadItems, ACL::Rename, ACL::AddItems, ACL::Delete, ACL::RemoveItems, ], ) .await .unwrap(); john_client .set_default_account_id(jane.id_string()) .mailbox_destroy(&mailbox_id, true) .await .unwrap(); // Try changing ACL assert_forbidden( john_client .set_default_account_id(jane.id_string()) .mailbox_update_acl(&inbox_id, bill.id_string(), [ACL::ReadItems]) .await, ); assert_forbidden( bill_client .set_default_account_id(jane.id_string()) .email_query(None::, None::>) .await, ); jane_client .mailbox_update_acl( &inbox_id, john.id_string(), [ ACL::ReadItems, ACL::AddItems, ACL::RemoveItems, ACL::SetKeywords, ACL::CreateChild, ACL::Rename, ACL::Administer, ], ) .await .unwrap(); assert_eq!( john_client .set_default_account_id(jane.id_string()) .mailbox_get(&inbox_id, [mailbox::Property::MyRights].into()) .await .unwrap() .unwrap() .my_rights() .unwrap() .acl_list(), vec![ ACL::ReadItems, ACL::AddItems, ACL::RemoveItems, ACL::SetSeen, ACL::SetKeywords, ACL::CreateChild, ACL::Rename ] ); john_client .set_default_account_id(jane.id_string()) .mailbox_update_acl(&inbox_id, bill.id_string(), [ACL::ReadItems]) .await .unwrap(); assert_eq!( bill_client .set_default_account_id(jane.id_string()) .email_query( None::, vec![email::query::Comparator::subject()].into() ) .await .unwrap() .ids(), [ email_ids.get("jane").unwrap().first().unwrap().as_str(), &email_id_2 ] ); // Revoke all access to John jane_client .mailbox_update_acl(&inbox_id, john.id_string(), []) .await .unwrap(); assert_forbidden( john_client .set_default_account_id(jane.id_string()) .email_get( email_ids.get("jane").unwrap().first().unwrap(), [Property::Subject].into(), ) .await, ); john_client.refresh_session().await.unwrap(); assert!(john_client.session().account(jane.id_string()).is_none()); assert_eq!( bill_client .set_default_account_id(jane.id_string()) .email_get( email_ids.get("jane").unwrap().first().unwrap(), [Property::Subject].into(), ) .await .unwrap() .unwrap() .subject() .unwrap(), "Owned by jane in inbox" ); // Add John and Jane to the Sales group let sales_id = test.account("sales@example.com").id(); for name in ["jdoe@example.com", "jane.smith@example.com"] { admin .registry_update_object( ObjectType::Account, test.account(name).id(), json!({ "memberGroupIds": { sales_id: true }, }), ) .await; } john_client.refresh_session().await.unwrap(); jane_client.refresh_session().await.unwrap(); bill_client.refresh_session().await.unwrap(); assert_eq!( john_client .session() .account(sales.id_string()) .unwrap() .name(), "sales@example.com" ); assert!( !john_client .session() .account(sales.id_string()) .unwrap() .is_personal() ); assert_eq!( jane_client .session() .account(sales.id_string()) .unwrap() .name(), "sales@example.com" ); assert!(bill_client.session().account(sales.id_string()).is_none()); // Insert a message in Sales's inbox let blob_id = john_client .set_default_account_id(sales.id_string()) .upload( Some(sales.id_string()), concat!( "From: acl_test@example.com\r\n", "To: sales@example.com\r\n", "Subject: Created by john in sales\r\n", "\r\n", "This message is owned by sales.", ) .as_bytes() .to_vec(), None, ) .await .unwrap() .take_blob_id(); let mut request = john_client.build(); let email_id = request .import_email() .email(&blob_id) .mailbox_ids([&inbox_id]) .create_id(); let email_id = request .send_single::() .await .unwrap() .created(&email_id) .unwrap() .take_id(); // Both Jane and John should be able to see this message, but not Bill assert_eq!( john_client .set_default_account_id(sales.id_string()) .email_get(&email_id, [Property::Subject].into(),) .await .unwrap() .unwrap() .subject() .unwrap(), "Created by john in sales" ); assert_eq!( jane_client .set_default_account_id(sales.id_string()) .email_get(&email_id, [Property::Subject].into(),) .await .unwrap() .unwrap() .subject() .unwrap(), "Created by john in sales" ); assert_forbidden( bill_client .set_default_account_id(sales.id_string()) .email_get(&email_id, [Property::Subject].into()) .await, ); // inbuxa: MA-D0: a member can't share the group's mailbox on, and isn't // told it may. Who is in a group is an administrator's decision. assert_forbidden( john_client .set_default_account_id(sales.id_string()) .mailbox_update_acl(&inbox_id, bill.id_string(), [ACL::ReadItems]) .await, ); assert!( !john_client .set_default_account_id(sales.id_string()) .mailbox_get(&inbox_id, [mailbox::Property::MyRights].into()) .await .unwrap() .unwrap() .my_rights() .unwrap() .acl_list() .contains(&ACL::Administer) ); bill_client.refresh_session().await.unwrap(); assert!(bill_client.session().account(sales.id_string()).is_none()); assert_forbidden( bill_client .set_default_account_id(sales.id_string()) .email_get(&email_id, [Property::Subject].into()) .await, ); // Remove John from the sales group admin .registry_update_object( ObjectType::Account, test.account("jdoe@example.com").id(), json!({ "memberGroupIds": { sales_id: false }, }), ) .await; assert_forbidden( john_client .set_default_account_id(sales.id_string()) .email_get(&email_id, [Property::Subject].into()) .await, ); // Destroy test account data for account in [john, bill, jane, sales] { admin .destroy_all_mailboxes_for_account(account.id().document_id()) .await; } test.assert_is_empty().await; } pub fn assert_forbidden(result: Result) { if !matches!( result, Err(jmap_client::Error::Method(MethodError { p_type: MethodErrorType::Forbidden })) | Err(jmap_client::Error::Set(SetError { type_: SetErrorType::BlobNotFound | SetErrorType::Forbidden, .. })) ) { panic!("Expected forbidden, got {:?}", result); } }