diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 95c82b4..3d75bc0 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -840,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 diff --git a/crates/jmap/src/file/copy.rs b/crates/jmap/src/file/copy.rs index 6cdddfd..d0e56db 100644 --- a/crates/jmap/src/file/copy.rs +++ b/crates/jmap/src/file/copy.rs @@ -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() diff --git a/crates/jmap/src/file/set.rs b/crates/jmap/src/file/set.rs index 2317df9..428eb55 100644 --- a/crates/jmap/src/file/set.rs +++ b/crates/jmap/src/file/set.rs @@ -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; } diff --git a/tests/src/system/account_lock.rs b/tests/src/system/account_lock.rs index 9b37538..9117751 100644 --- a/tests/src/system/account_lock.rs +++ b/tests/src/system/account_lock.rs @@ -193,6 +193,15 @@ pub async fn test(test: &mut TestServer) { 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( @@ -236,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