Don't let a group's members share its calendars, address books or files
#146 stopped a group's members sharing its mailboxes on. The same shortcut lets them through everywhere else a group owns things: a member counts as the account's owner, so Calendar/set, AddressBook/set and FileNode/set skip the share check, and so does the WebDAV ACL method. Who has what a group owns is decided by who is in the group. For a member through a group only (is_group_member_only): - Calendar/set, AddressBook/set and FileNode/set refuse a shareWith change as forbidden, on create and update; for files at the top of the account too, not only inside a folder; - the DAV ACL method answers 403 on the group's calendars, address books and files; - myRights reports mayShare false (JmapRights::owner_rights), and the DAV current-user-privilege-set leaves out all and write-acl. Reading who something is shared with is unchanged, as in JMAP. Tests: a new jmap::group_share module has a member create with a share, create without one (and check myRights), share afterwards, and an outsider reach each kind; the WebDAV ACL test has a member try the ACL method on the group's folders; the IMAP ACL test now checks #146's SETACL refusal, which had no test of its own. jmap_tests, webdav_tests and imap_tests pass (RocksDB). specs/multi-account.md MA-D0.
This commit is contained in:
1 parent
5f6548bfdd
commit
58d2804278
12 files changed
+267
-4
No files matched your search
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! MA-D0 (specs/multi-account.md): a group's members have its calendars,
|
||||
//! address books and files, but can't share them on. Who is in a group is
|
||||
//! an administrator's decision. Mailboxes are checked in `mail::acl`.
|
||||
|
||||
use crate::utils::{jmap::JmapUtils, server::TestServer};
|
||||
use jmap_proto::request::method::MethodObject;
|
||||
use registry::schema::prelude::ObjectType;
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn test(test: &TestServer) {
|
||||
println!("Running group sharing tests...");
|
||||
let admin = test.account("[email protected]");
|
||||
let sales = test.account("[email protected]");
|
||||
let bill = test.account("[email protected]");
|
||||
let robert = test.account("[email protected]");
|
||||
let robert_id = robert.id_string().to_string();
|
||||
|
||||
// Bill joins the group; Robert stays outside it
|
||||
admin
|
||||
.registry_update_object(
|
||||
ObjectType::Account,
|
||||
bill.id(),
|
||||
json!({"memberGroupIds": {sales.id_string(): true}}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Each kind's own name for "may read"
|
||||
for (object, read) in [
|
||||
(MethodObject::Calendar, "mayReadItems"),
|
||||
(MethodObject::AddressBook, "mayRead"),
|
||||
(MethodObject::FileNode, "mayRead"),
|
||||
] {
|
||||
// Made with a share: refused
|
||||
let response = bill
|
||||
.jmap_create_account(
|
||||
sales,
|
||||
object,
|
||||
[json!({
|
||||
"name": "Shared on",
|
||||
"shareWith": {&robert_id: {read: true}}
|
||||
})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.pointer("/methodResponses/0/1/notCreated/i0/type"),
|
||||
Some(&json!("forbidden")),
|
||||
"MA-D0: {object} created with a share: {:?}",
|
||||
response.pointer("/methodResponses/0")
|
||||
);
|
||||
|
||||
// Made without one: fine, and it says it can't be shared
|
||||
let id = bill
|
||||
.jmap_create_account(
|
||||
sales,
|
||||
object,
|
||||
[json!({"name": "The group's"})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.created(0)
|
||||
.id()
|
||||
.to_string();
|
||||
let rights = bill
|
||||
.jmap_get_account(sales, object, ["myRights"], [id.as_str()])
|
||||
.await
|
||||
.list()[0]["myRights"]
|
||||
.clone();
|
||||
assert_eq!(rights["mayShare"], false, "MA-D0: {object} myRights {rights}");
|
||||
assert_eq!(rights["mayDelete"], true, "MA-D0: {object} myRights {rights}");
|
||||
|
||||
// Shared afterwards: refused
|
||||
let response = bill
|
||||
.jmap_update_account(
|
||||
sales,
|
||||
object,
|
||||
[(
|
||||
&id,
|
||||
json!({format!("shareWith/{robert_id}"): {read: true}}),
|
||||
)],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
response.pointer(&format!("/methodResponses/0/1/notUpdated/{id}/type")),
|
||||
Some(&json!("forbidden")),
|
||||
"MA-D0: {object} shared on: {:?}",
|
||||
response.pointer("/methodResponses/0")
|
||||
);
|
||||
|
||||
// Robert still has nothing
|
||||
assert_eq!(
|
||||
robert
|
||||
.jmap_get_account(sales, object, Vec::<&str>::new(), [id.as_str()])
|
||||
.await
|
||||
.method_response()
|
||||
.typ(),
|
||||
"forbidden",
|
||||
"MA-D0: {object} reached from outside"
|
||||
);
|
||||
|
||||
bill.jmap_destroy_account(sales, object, [id.as_str()], Vec::<(&str, &str)>::new())
|
||||
.await;
|
||||
}
|
||||
|
||||
// Reaching the group's calendars and address books made its defaults
|
||||
let sales_id = sales.id_string();
|
||||
bill.jmap_method_calls(json!([
|
||||
["Calendar/get", {"accountId": sales_id, "ids": (), "properties": ["id"]}, "c"],
|
||||
["Calendar/set", {"accountId": sales_id, "onDestroyRemoveEvents": true,
|
||||
"#destroy": {"resultOf": "c", "name": "Calendar/get", "path": "/list/*/id"}}, "cd"],
|
||||
["AddressBook/get", {"accountId": sales_id, "ids": (), "properties": ["id"]}, "a"],
|
||||
["AddressBook/set", {"accountId": sales_id, "onDestroyRemoveContents": true,
|
||||
"#destroy": {"resultOf": "a", "name": "AddressBook/get", "path": "/list/*/id"}}, "ad"]
|
||||
]))
|
||||
.await;
|
||||
|
||||
admin
|
||||
.registry_update_object(
|
||||
ObjectType::Account,
|
||||
bill.id(),
|
||||
json!({"memberGroupIds": {sales.id_string(): false}}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Reference in new issue
Block a user