Compare commits

..
Author SHA1 Message Date
jcoffey-dev 621ebdff74 Merge pull request 'Release 2026.9.27.2' (#69) from release-2026.9.27.2 into main
publish / publish-arm64 (push) Successful in 39m17s
publish / binaries (push) Successful in 33s
publish / announce (push) Successful in 23s
ci / fork-checks (push) Successful in 14s
publish / version (push) Successful in 32s
publish / publish-amd64 (push) Successful in 25m41s
publish / release (push) Successful in 1s
ci / build (push) Successful in 37m4s
2026-09-28 01:35:26 +00:00
jcoffey-dev 355bd3a40e Release 2026.9.27.2
ci / fork-checks (pull_request) Successful in 1m23s
ci / build (pull_request) Successful in 7m16s
Delegates reach the whole locked account (#68): its calendars, contacts
and files as well as its mail, even a kind it holds none of yet, and
writing delegates may add at the top of its Files.

Prepared Explain answers relabeled for this release; no setting changed
since 2026.9.27.1, so all 706 carry over.
2026-09-27 18:27:33 -07:00
jcoffey-dev f7a63b9ed0 Merge pull request 'Delegates reach the whole locked account' (#68) from fix/delegate-whole-account into main
ci / fork-checks (push) Successful in 48s
ci / build (push) Canceled after 12m39s
2026-09-28 01:22:48 +00:00
jcoffey-dev 9f6761c9dd Writing delegates may add at the top of a locked account's Files
ci / fork-checks (pull_request) Successful in 17s
ci / build (pull_request) Successful in 17m56s
A shared account refuses top-level folders, so an organize or full
delegate couldn't add anything to a locked account with no folders. A
delegate who may write now can, as the owner could; the reconcile after
the create grants it the new folder. Read delegates still can't (AL-6,
AL-7).
2026-09-27 18:04:24 -07:00
jcoffey-dev d4d127fa7d Delegates reach the whole locked account
ci / build (pull_request) Canceled after 5m27s
ci / fork-checks (pull_request) Successful in 14s
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).
2026-09-27 17:58:50 -07:00
6 changed files with 93 additions and 8 deletions
+30
View File
@@ -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
+9 -4
View File
@@ -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()
+9 -3
View File
@@ -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;
}
+1 -1
View File
@@ -81,7 +81,7 @@ fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option<String> {
#[macro_export]
macro_rules! brand_version {
() => {
"2026.9.27.1"
"2026.9.27.2"
};
}
Binary file not shown.
+44
View File
@@ -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