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]