This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch 3.9.x
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/3.9.x by this push:
     new 7e4d68801b [ENHANCEMENT] Implement negative ACL for JMAP (cherry 
picked from commit f800b484b51008e5de98258dae22a978a9e1ccf1) (#3057)
7e4d68801b is described below

commit 7e4d68801b6c58754e0227a55e84af0db4cfe485
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Jun 4 09:13:19 2026 +0200

    [ENHANCEMENT] Implement negative ACL for JMAP (cherry picked from commit 
f800b484b51008e5de98258dae22a978a9e1ccf1) (#3057)
---
 .../james/mailbox/store/StoreRightManager.java     | 19 +++---
 .../james/mailbox/store/StoreRightManagerTest.java | 71 +++++++++++++++++++++-
 2 files changed, 80 insertions(+), 10 deletions(-)

diff --git 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
index 49ad62fe69..36aaf093da 100644
--- 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
+++ 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreRightManager.java
@@ -278,10 +278,12 @@ public class StoreRightManager implements RightManager {
 
     private void assertHaveAccessTo(Mailbox mailbox, MailboxSession session) 
throws InsufficientRightsException, MailboxNotFoundException {
         if (!mailbox.generateAssociatedPath().belongsTo(session)) {
-            Rfc4314Rights acl = 
mailbox.getACL().getEntries().get(EntryKey.createUserEntryKey(session.getUser()));
-            if (acl == null) {
+            // Use the effective rights (positive entries minus applicable 
negative entries) rather
+            // than the raw positive ACL entry, so that a negative ACL 
removing Administer is honoured.
+            Rfc4314Rights rights = myRights(mailbox, session);
+            if (rights.isEmpty()) {
                 throw new MailboxNotFoundException(mailbox.getMailboxId());
-            } else if (!acl.contains(Right.Administer)) {
+            } else if (!rights.contains(Right.Administer)) {
                 throw new InsufficientRightsException("Setting ACL is only 
permitted to the owner and admins of the mailbox");
             }
         }
@@ -333,17 +335,20 @@ public class StoreRightManager implements RightManager {
      * the connected user.
      */
     @VisibleForTesting
-    static MailboxACL filteredForSession(Mailbox mailbox, MailboxACL acl, 
MailboxSession mailboxSession) {
+    MailboxACL filteredForSession(Mailbox mailbox, MailboxACL acl, 
MailboxSession mailboxSession) throws UnsupportedRightException {
         if (mailbox.generateAssociatedPath().belongsTo(mailboxSession)) {
             return acl;
         }
 
         MailboxACL.EntryKey userAsKey = 
MailboxACL.EntryKey.createUserEntryKey(mailboxSession.getUser());
-        Rfc4314Rights rights = acl.getEntries().getOrDefault(userAsKey, new 
Rfc4314Rights());
-        if (rights.contains(MailboxACL.Right.Administer)) {
+        // Expose the full ACL only when the user effectively holds Administer 
(i.e. after applicable
+        // negative ACL entries have been subtracted), not merely from their 
raw positive entry.
+        Rfc4314Rights effectiveRights = 
aclResolver.resolveRights(mailboxSession.getUser(), acl, mailbox.getUser());
+        if (effectiveRights.contains(MailboxACL.Right.Administer)) {
             return acl;
         }
-        return new MailboxACL(ImmutableMap.of(userAsKey, rights));
+        Rfc4314Rights ownRights = acl.getEntries().getOrDefault(userAsKey, new 
Rfc4314Rights());
+        return new MailboxACL(ImmutableMap.of(userAsKey, ownRights));
     }
 
     /** Sets an ACL for a mailbox *WITHOUT* checking if the user of current 
session is allowed to do so.
diff --git 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
index cb5b7f7b75..ace59dd966 100644
--- 
a/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
+++ 
b/mailbox/store/src/test/java/org/apache/james/mailbox/store/StoreRightManagerTest.java
@@ -42,6 +42,7 @@ import org.apache.james.mailbox.acl.MailboxACLResolver;
 import org.apache.james.mailbox.acl.UnionMailboxACLResolver;
 import org.apache.james.mailbox.events.MailboxIdRegistrationKey;
 import org.apache.james.mailbox.exception.DifferentDomainException;
+import org.apache.james.mailbox.exception.InsufficientRightsException;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.exception.UnsupportedRightException;
@@ -49,6 +50,7 @@ import org.apache.james.mailbox.fixture.MailboxFixture;
 import org.apache.james.mailbox.model.Mailbox;
 import org.apache.james.mailbox.model.MailboxACL;
 import org.apache.james.mailbox.model.MailboxACL.ACLCommand;
+import org.apache.james.mailbox.model.MailboxACL.EntryKey;
 import org.apache.james.mailbox.model.MailboxACL.Right;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxId;
@@ -214,7 +216,7 @@ class StoreRightManagerTest {
         MailboxACL acl = new MailboxACL()
             .apply(MailboxACL.command().rights(Right.Read, 
Right.Write).forUser(BOB).asAddition())
             .apply(MailboxACL.command().rights(Right.Read, Right.Write, 
Right.Administer).forUser(CEDRIC).asAddition());
-        MailboxACL actual = StoreRightManager.filteredForSession(
+        MailboxACL actual = storeRightManager.filteredForSession(
             new Mailbox(INBOX_ALICE, UID_VALIDITY, MAILBOX_ID), acl, 
aliceSession);
         assertThat(actual).isEqualTo(acl);
     }
@@ -224,7 +226,7 @@ class StoreRightManagerTest {
         MailboxACL acl = new MailboxACL()
             .apply(MailboxACL.command().rights(Right.Read, 
Right.Write).forUser(BOB).asAddition())
             .apply(MailboxACL.command().rights(Right.Read, Right.Write, 
Right.Administer).forUser(CEDRIC).asAddition());
-        MailboxACL actual = StoreRightManager.filteredForSession(
+        MailboxACL actual = storeRightManager.filteredForSession(
             new Mailbox(INBOX_ALICE, UID_VALIDITY, MAILBOX_ID), acl, 
MailboxSessionUtil.create(CEDRIC));
         assertThat(actual).isEqualTo(acl);
     }
@@ -234,7 +236,7 @@ class StoreRightManagerTest {
         MailboxACL acl = new MailboxACL()
             .apply(MailboxACL.command().rights(Right.Read, 
Right.Write).forUser(BOB).asAddition())
             .apply(MailboxACL.command().rights(Right.Read, Right.Write, 
Right.Administer).forUser(CEDRIC).asAddition());
-        MailboxACL actual = StoreRightManager.filteredForSession(
+        MailboxACL actual = storeRightManager.filteredForSession(
             new Mailbox(INBOX_ALICE, UID_VALIDITY, MAILBOX_ID), acl, 
MailboxSessionUtil.create(BOB));
         
assertThat(actual.getEntries()).containsKey(MailboxACL.EntryKey.createUserEntryKey(BOB));
     }
@@ -335,4 +337,67 @@ class StoreRightManagerTest {
             StoreRightManager.IS_CROSS_DOMAIN_ACCESS_ALLOWED = false;
         }
     }
+
+    @Test
+    void 
applyRightsCommandShouldThrowWhenAdministerIsRemovedByANegativeAclEntry() 
throws Exception {
+        // Alice holds a raw positive Administer grant, but an applicable 
negative ACL entry
+        // (as can be set through IMAP SETACL "-alice a") removes Administer 
from her effective rights.
+        MailboxPath mailboxPath = MailboxPath.forUser(BOB, "mailbox");
+        Mailbox mailbox = new Mailbox(mailboxPath, UID_VALIDITY, MAILBOX_ID);
+        mailbox.setACL(new MailboxACL()
+            .apply(MailboxACL.command().forUser(ALICE).rights(Right.Lookup, 
Right.Administer).asAddition())
+            .apply(MailboxACL.command().key(EntryKey.createUserEntryKey(ALICE, 
true)).rights(Right.Administer).asAddition()));
+
+        // Sanity check: Alice's effective rights no longer contain Administer
+        assertThat(storeRightManager.hasRight(mailbox, Right.Administer, 
aliceSession)).isFalse();
+
+        ACLCommand aclCommand = MailboxACL.command()
+            .forUser(ALICE)
+            .rights(Right.Read)
+            .asAddition();
+
+        
when(mockedMailboxMapper.findMailboxByPath(mailboxPath)).thenReturn(Mono.just(mailbox));
+
+        assertThatThrownBy(() -> 
storeRightManager.applyRightsCommand(mailboxPath, aclCommand, aliceSession))
+            .isInstanceOf(InsufficientRightsException.class);
+    }
+
+    @Test
+    void applyRightsCommandShouldNotThrowWhenEffectiveAdministerIsHeld() 
throws Exception {
+        // Baseline counterpart: with a positive Administer grant and no 
negating entry,
+        // the mutation is legitimately authorized.
+        MailboxPath mailboxPath = MailboxPath.forUser(BOB, "mailbox");
+        Mailbox mailbox = new Mailbox(mailboxPath, UID_VALIDITY, MAILBOX_ID);
+        mailbox.setACL(new MailboxACL(new MailboxACL.Entry(ALICE.asString(), 
Right.Lookup, Right.Administer)));
+
+        assertThat(storeRightManager.hasRight(mailbox, Right.Administer, 
aliceSession)).isTrue();
+
+        ACLCommand aclCommand = MailboxACL.command()
+            .forUser(ALICE)
+            .rights(Right.Read)
+            .asAddition();
+
+        
when(mockedMailboxMapper.findMailboxByPath(mailboxPath)).thenReturn(Mono.just(mailbox));
+        when(mockedMailboxMapper.updateACL(mailbox, 
aclCommand)).thenReturn(Mono.just(ACLDiff.computeDiff(MailboxACL.EMPTY, 
MailboxACL.EMPTY)));
+        when(eventBus.dispatch(any(Event.class), 
any(MailboxIdRegistrationKey.class))).thenReturn(Mono.empty());
+
+        assertThatCode(() -> storeRightManager.applyRightsCommand(mailboxPath, 
aclCommand, aliceSession))
+            .doesNotThrowAnyException();
+    }
+
+    @Test
+    void 
filteredForSessionShouldHideOtherEntriesWhenAdministerIsRemovedByANegativeAclEntry()
 throws UnsupportedRightException {
+        // BOB holds a raw positive Administer grant negated by an applicable 
negative ACL entry:
+        // his effective Administer right is false, so he must not be granted 
full-ACL visibility.
+        MailboxACL acl = new MailboxACL()
+            .apply(MailboxACL.command().forUser(BOB).rights(Right.Lookup, 
Right.Administer).asAddition())
+            .apply(MailboxACL.command().key(EntryKey.createUserEntryKey(BOB, 
true)).rights(Right.Administer).asAddition())
+            .apply(MailboxACL.command().rights(Right.Read, Right.Write, 
Right.Administer).forUser(CEDRIC).asAddition());
+
+        MailboxACL actual = storeRightManager.filteredForSession(
+            new Mailbox(INBOX_ALICE, UID_VALIDITY, MAILBOX_ID), acl, 
MailboxSessionUtil.create(BOB));
+
+        assertThat(actual.getEntries())
+            .doesNotContainKey(EntryKey.createUserEntryKey(CEDRIC));
+    }
 }
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to