Delegates reach the whole locked account #68
@@ -143,6 +143,29 @@ impl Server {
|
||||
}
|
||||
}
|
||||
}
|
||||
// inbuxa: AL-7: a delegate reaches the whole locked account,
|
||||
// mail, calendars, contacts and files, even a kind it holds
|
||||
// none of yet, so an empty one reads as empty rather than
|
||||
// refused. What it may see or change there is still each
|
||||
// container's grant.
|
||||
for delegation in delegations.iter() {
|
||||
let whole: Bitmap<Collection> = Bitmap::from_iter([
|
||||
Collection::Mailbox,
|
||||
Collection::Email,
|
||||
Collection::Calendar,
|
||||
Collection::CalendarEvent,
|
||||
Collection::AddressBook,
|
||||
Collection::ContactCard,
|
||||
Collection::FileNode,
|
||||
]);
|
||||
match access_to.iter_mut().find(|a| a.account_id == delegation.account_id) {
|
||||
Some(entry) => entry.collections.union(&whole),
|
||||
None => access_to.push(AccessTo {
|
||||
account_id: delegation.account_id,
|
||||
collections: whole,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
let now = now();
|
||||
let mut credential_version = 0;
|
||||
@@ -817,6 +840,13 @@ impl AccessToken {
|
||||
|
||||
/// inbuxa: AL-5: this account's delegation into a locked account, if it
|
||||
/// has one that hasn't ended.
|
||||
/// inbuxa: AL-6, AL-7: a delegate at organize or full, who may add to
|
||||
/// the locked account as its owner could, top-level folders included.
|
||||
pub fn delegate_may_write(&self, account_id: u32) -> bool {
|
||||
self.delegation(account_id)
|
||||
.is_some_and(|d| d.access != inbuxa_features::lock::Access::Read)
|
||||
}
|
||||
|
||||
pub fn delegation(&self, account_id: u32) -> Option<&super::Delegation> {
|
||||
let now = now();
|
||||
self.inner
|
||||
|
||||
@@ -226,9 +226,14 @@ impl FileNodeCopy for Server {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) =
|
||||
validate_file_node_hierarchy(None, &file_node, is_shared, &cache, &created_folders)
|
||||
{
|
||||
// inbuxa: AL-7: a writing delegate may add at the top
|
||||
if let Err(err) = validate_file_node_hierarchy(
|
||||
None,
|
||||
&file_node,
|
||||
is_shared && !access_token.delegate_may_write(account_id),
|
||||
&cache,
|
||||
&created_folders,
|
||||
) {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
@@ -362,7 +367,7 @@ impl FileNodeCopy for Server {
|
||||
);
|
||||
continue 'create;
|
||||
}
|
||||
} else if is_shared {
|
||||
} else if is_shared && !access_token.delegate_may_write(account_id) {
|
||||
response.not_created.append(
|
||||
id,
|
||||
SetError::forbidden()
|
||||
|
||||
@@ -149,9 +149,15 @@ impl FileNodeSet for Server {
|
||||
};
|
||||
|
||||
// Validate hierarchy
|
||||
if let Err(err) =
|
||||
validate_file_node_hierarchy(None, &file_node, is_shared, &cache, &created_folders)
|
||||
{
|
||||
// inbuxa: AL-7: a writing delegate may add at the top of a
|
||||
// locked account, which may hold no folders at all
|
||||
if let Err(err) = validate_file_node_hierarchy(
|
||||
None,
|
||||
&file_node,
|
||||
is_shared && !access_token.delegate_may_write(account_id),
|
||||
&cache,
|
||||
&created_folders,
|
||||
) {
|
||||
response.not_created.append(id, err);
|
||||
continue 'create;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ const USING: &[&str] = &[
|
||||
"urn:ietf:params:jmap:core",
|
||||
"urn:ietf:params:jmap:mail",
|
||||
"urn:ietf:params:jmap:submission",
|
||||
"urn:ietf:params:jmap:calendars",
|
||||
"urn:ietf:params:jmap:contacts",
|
||||
"urn:ietf:params:jmap:filenode",
|
||||
"urn:inbuxa:jmap",
|
||||
];
|
||||
|
||||
@@ -179,6 +182,26 @@ pub async fn test(test: &mut TestServer) {
|
||||
assert_eq!(delegation["access"], "read", "AL-7");
|
||||
assert_eq!(delegation["sendAs"], false, "AL-7");
|
||||
|
||||
// AL-7: the whole account, not only mail: even a kind the owner holds
|
||||
// none of (no files here) reads as empty rather than refused
|
||||
for (method, arguments) in [
|
||||
("FileNode/query", json!({"accountId": owner_id})),
|
||||
("Calendar/get", json!({"accountId": owner_id, "ids": null})),
|
||||
("AddressBook/get", json!({"accountId": owner_id, "ids": null})),
|
||||
] {
|
||||
let (name, response) = delegate.call(method, arguments).await;
|
||||
assert_eq!(name, method, "AL-7: {method} refused to the delegate: {response}");
|
||||
}
|
||||
|
||||
// AL-6: reading adds nothing, not even at the top of empty Files
|
||||
let (_, response) = delegate
|
||||
.call(
|
||||
"FileNode/set",
|
||||
json!({"accountId": owner_id, "create": {"f": {"name": "Notes", "parentId": null}}}),
|
||||
)
|
||||
.await;
|
||||
assert!(response["created"].get("f").is_none(), "AL-6: a read delegate added a file: {response}");
|
||||
|
||||
// The delegate reads the mail that arrived
|
||||
let (_, found) = delegate
|
||||
.call(
|
||||
@@ -222,6 +245,27 @@ pub async fn test(test: &mut TestServer) {
|
||||
"AL-5: {response}"
|
||||
);
|
||||
|
||||
// AL-7: organize adds at the top of the locked account's Files, which
|
||||
// held none, and sees what it made
|
||||
let (_, response) = delegate
|
||||
.call(
|
||||
"FileNode/set",
|
||||
json!({"accountId": owner_id, "create": {"f": {"name": "Handover notes", "parentId": null}}}),
|
||||
)
|
||||
.await;
|
||||
let folder_id = response["created"]["f"]["id"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("AL-7: organize couldn't add to empty Files: {response}"))
|
||||
.to_string();
|
||||
let (_, response) = delegate
|
||||
.call("FileNode/get", json!({"accountId": owner_id, "ids": [folder_id]}))
|
||||
.await;
|
||||
assert_eq!(
|
||||
response["list"].as_array().map(Vec::len),
|
||||
Some(1),
|
||||
"AL-7: the delegate can't see the folder it made: {response}"
|
||||
);
|
||||
|
||||
// Test 12, AL-6, AL-7: organize makes folders it can see, moves mail,
|
||||
// never deletes it
|
||||
let (_, mailboxes) = delegate
|
||||
|
||||
Reference in New Issue
Block a user