From d4d127fa7deca03786ab346acd50bf7340c32bb5 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sun, 27 Sep 2026 17:58:50 -0700 Subject: [PATCH] Delegates reach the whole locked account A delegate's token listed the locked account only for kinds of data it held grants on, so one with no files (or no calendar) was refused to the delegate outright: "You do not have access to account". The token now lists the locked account for mail, calendars, contacts and files alike, so an empty kind reads as empty. What the delegate may see or change is still each container's grant (AL-7). --- crates/common/src/auth/access_token.rs | 23 +++++++++++++++++++++++ tests/src/system/account_lock.rs | 14 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 408746c..95c82b4 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -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 = 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; diff --git a/tests/src/system/account_lock.rs b/tests/src/system/account_lock.rs index 40067d0..9b37538 100644 --- a/tests/src/system/account_lock.rs +++ b/tests/src/system/account_lock.rs @@ -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,17 @@ 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}"); + } + // The delegate reads the mail that arrived let (_, found) = delegate .call(