Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JAMES-2182 Sharing for IMAP #2445

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0654862
[REFACTORING] drop unused logger
chibenwa Oct 9, 2024
becc897
JAMES-2182 Remove ImapSession::supportMultipleNamespaces
chibenwa Oct 9, 2024
9ee964c
JAMES-2182 Inline namespace related concerns out of the mailbox session
chibenwa Oct 9, 2024
bd127c2
JAMES-2182 NamespaceResponse: rely on collection rather than list
chibenwa Oct 9, 2024
5419e3f
JAMES-2182 NamespaceProcessorTest: remove the empty test class
chibenwa Oct 9, 2024
84f2ee6
JAMES-2182 Inject NamespaceSupplier into its processor
chibenwa Oct 9, 2024
6ad4025
JAMES-2182 ListProcessor: fix checkstyles
chibenwa Oct 9, 2024
a79b7f4
JAMES-2182 PathConverter: transform into an interface and inject
chibenwa Oct 9, 2024
e32080d
JAMES-2182 PathConverter: Add the reverse transformation MailboxPath …
chibenwa Oct 9, 2024
5bf1a43
JAMES-2182 PathConverter: change arguments of mailboxName method
chibenwa Oct 9, 2024
16e8ef7
JAMES-2182 PathConverter: only mailboxes belonging to the user can be…
chibenwa Oct 9, 2024
d96933c
JAMES-2182 PathConverter: more unit tests
chibenwa Oct 9, 2024
c3df25b
JAMES-2182 PathConverter: handle encoding for mailboxes belonging to …
chibenwa Oct 9, 2024
66f6b80
JAMES-2182 PathConverter: handle virtual hosting
chibenwa Oct 9, 2024
ad0b90a
JAMES-2182 PathConverter: username escaping for dots
chibenwa Oct 9, 2024
08b59b4
JAMES-2182 Partial implementation for shared folders in IMAP
chibenwa Oct 9, 2024
9767861
JAMES-2182 MailboxManager mailbox search for specific other user
chibenwa Oct 9, 2024
38c81f6
JAMES-2182 ListProcessor: handle split in reference between #user and…
chibenwa Oct 9, 2024
4b43fa0
JAMES-2182 ListProcessor: extract ListRequest -> MailboxQuery convert…
chibenwa Oct 9, 2024
77d2756
JAMES-2182 Only user folder may be special use
chibenwa Oct 9, 2024
7a906f5
JAMES-2182 List myrights response should preserve namespace
chibenwa Oct 9, 2024
6cee7a8
JAMES-2182 Fix checkstyles
chibenwa Oct 9, 2024
b882b7d
JAMES-2182 LSUB for delegated accounts
chibenwa Oct 10, 2024
7199458
JAMES-2182 Allow Read only selects
chibenwa Oct 10, 2024
f845583
[REFACTORING] CreateProcessor: inline unneeded flatMap
chibenwa Oct 10, 2024
75b29e8
[REFACTORING] SystemMessageProcessor: remove unneeded fields
chibenwa Oct 10, 2024
a319c8f
[REFACTORING] Tests for UNSUBSCRIBE
chibenwa Oct 10, 2024
554dba7
JAMES-2182 PathConverter: use MailboxSession where more convenient
chibenwa Oct 10, 2024
233f20c
JAMES-2182 Fix InMemorySecurityTest
chibenwa Oct 11, 2024
631e9d1
JAMES-2182 Base test suite regarding IMAP right enforcements
chibenwa Oct 11, 2024
67f82fb
JAMES-2182 Fix rights for CREATE
chibenwa Oct 17, 2024
48aa43a
JAMES-2182 Fix rights for DELETE
chibenwa Oct 17, 2024
8a93a83
JAMES-2182 Fix rights for SETACL
chibenwa Oct 17, 2024
c935acf
JAMES-2182 Fix rights for APPEND, MOVE, COPY
chibenwa Oct 17, 2024
f1946c9
JAMES-2182 Fix rights for SELECT, STATUS
chibenwa Oct 17, 2024
0fe3a6f
JAMES-2182 Fix rights for STORE
chibenwa Oct 17, 2024
ff04821
JAMES-2128 Ensure creating #user is forbidden
chibenwa Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

package org.apache.james.mailbox;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
Expand All @@ -29,7 +27,6 @@
import java.util.Optional;

import org.apache.james.core.Username;
import org.apache.james.mailbox.model.MailboxConstants;

import com.google.common.base.MoreObjects;

Expand Down Expand Up @@ -100,9 +97,6 @@ public enum SessionType {
User
}

private final Collection<String> sharedSpaces;
private final String otherUsersSpace;
private final String personalSpace;
private final SessionId sessionId;
private final Username userName;
private final Optional<Username> loggedInUser;
Expand All @@ -113,21 +107,9 @@ public enum SessionType {

public MailboxSession(SessionId sessionId, Username userName, Optional<Username> loggedInUser,
List<Locale> localePreferences, char pathSeparator, SessionType type) {
this(sessionId, userName, loggedInUser, localePreferences, new ArrayList<>(), null, pathSeparator, type);
}

public MailboxSession(SessionId sessionId, Username userName, Optional<Username> loggedInUser,
List<Locale> localePreferences, List<String> sharedSpaces, String otherUsersSpace, char pathSeparator, SessionType type) {
this.sessionId = sessionId;
this.userName = userName;
this.otherUsersSpace = otherUsersSpace;
this.sharedSpaces = sharedSpaces;
this.type = type;
if (otherUsersSpace == null && (sharedSpaces == null || sharedSpaces.isEmpty())) {
this.personalSpace = "";
} else {
this.personalSpace = MailboxConstants.USER_NAMESPACE;
}

this.localePreferences = localePreferences;
this.attributes = new HashMap<>();
Expand Down Expand Up @@ -173,43 +155,6 @@ public List<Locale> getLocalePreferences() {
return localePreferences;
}

/**
* Gets the <a href='http://www.isi.edu/in-notes/rfc2342.txt' rel='tag'>RFC
* 2342</a> personal namespace for the current session.<br>
* Note that though servers may offer multiple personal namespaces, support
* is not offered through this API. This decision may be revised if
* reasonable use cases emerge.
*
* @return Personal Namespace, not null
*/
public String getPersonalSpace() {
return personalSpace;
}

/**
* Gets the <a href='http://www.isi.edu/in-notes/rfc2342.txt' rel='tag'>RFC
* 2342</a> other users namespace for the current session.<br>
* Note that though servers may offer multiple other users namespaces,
* support is not offered through this API. This decision may be revised if
* reasonable use cases emerge.
*
* @return Other Users Namespace or null when there is non available
*/
public String getOtherUsersSpace() {
return otherUsersSpace;
}

/**
* Iterates the <a href='http://www.isi.edu/in-notes/rfc2342.txt'
* rel='tag'>RFC 2342</a> Shared Namespaces available for the current
* session.
*
* @return not null though possibly empty
*/
public Collection<String> getSharedSpaces() {
return sharedSpaces;
}

/**
* Return the stored attributes for this {@link MailboxSession}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ public String getName() {
}

public boolean belongsTo(MailboxSession mailboxSession) {
return user.equals(mailboxSession.getUser());
return Optional.ofNullable(user)
.map(mailboxSession.getUser()::equals)
.orElse(false);
}

public MailboxPath child(String childName, char delimiter) {
Expand Down Expand Up @@ -255,7 +257,7 @@ boolean hasEmptyNameInHierarchy(char pathDelimiter) {
}

public String asString() {
return namespace + ":" + user.asString() + ":" + name;
return namespace + ":" + Optional.ofNullable(user).map(Username::asString).orElse("") + ":" + name;
}

public String asEscapedString() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,18 @@ public PrefixedRegex(String prefix, String regex, char pathDelimiter) {

@Override
public boolean isExpressionMatch(String name) {
return name.startsWith(prefix)
&& regexMatching(name.substring(prefix.length()));
if (name.startsWith(prefix)) {
String nameSubstring = sanitizeSubstring(name.substring(prefix.length()));
return regexMatching(nameSubstring);
}
return false;
}

private String sanitizeSubstring(String name) {
if (!name.isEmpty() && name.charAt(0) == pathDelimiter) {
return name.substring(1);
}
return name;
}

private boolean regexMatching(String name) {
Expand Down Expand Up @@ -133,4 +143,12 @@ public final boolean equals(Object o) {
public final int hashCode() {
return Objects.hash(prefix, regex, pathDelimiter);
}

@Override
public String toString() {
return "PrefixedRegex{" +
"prefix='" + prefix + '\'' +
", regex='" + regex + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,96 @@ void hasInboxShouldBeTrueWhenINBOXIsCreated() throws Exception {
assertThat(mailboxId.get()).isEqualTo(retrievedMailbox.getId());
}

@Test
void shareeShouldBeAbleToCreateMailbox() throws Exception {
assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL));
session = mailboxManager.createSystemSession(USER_1);
MailboxPath mailboxPath = MailboxPath.inbox(session);
mailboxManager.createMailbox(mailboxPath, session);
mailboxManager.applyRightsCommand(mailboxPath,
MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_2))
.rights(MailboxACL.Rfc4314Rights.of(ImmutableList.of(MailboxACL.Right.Lookup,
MailboxACL.Right.Read, MailboxACL.Right.CreateMailbox)))
.asAddition(), session);

MailboxSession session2 = mailboxManager.createSystemSession(USER_2);
MailboxPath childPath = MailboxPath.inbox(session).child("child", session2.getPathDelimiter());
mailboxManager.createMailbox(childPath, session2);

assertThat(mailboxManager.getMailbox(childPath, session)
.getMailboxEntity().getACL().getEntries().get(MailboxACL.EntryKey.createUserEntryKey(USER_2)))
.isEqualTo(MailboxACL.Rfc4314Rights.fromSerializedRfc4314Rights("lrk"));
}

@Test
void shareeShouldBeAbleToCreateMailboxChildren() throws Exception {
assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL));
session = mailboxManager.createSystemSession(USER_1);
MailboxPath mailboxPath = MailboxPath.inbox(session);
mailboxManager.createMailbox(mailboxPath, session);
mailboxManager.applyRightsCommand(mailboxPath,
MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_2))
.rights(MailboxACL.Rfc4314Rights.of(ImmutableList.of(MailboxACL.Right.Lookup,
MailboxACL.Right.Read, MailboxACL.Right.CreateMailbox)))
.asAddition(), session);

MailboxSession session2 = mailboxManager.createSystemSession(USER_2);
MailboxPath childPath = MailboxPath.inbox(session)
.child("child", session2.getPathDelimiter())
.child("anotherkid", session2.getPathDelimiter());
mailboxManager.createMailbox(childPath, session2);

assertThat(mailboxManager.getMailbox(childPath, session)
.getMailboxEntity().getACL().getEntries().get(MailboxACL.EntryKey.createUserEntryKey(USER_2)))
.isEqualTo(MailboxACL.Rfc4314Rights.fromSerializedRfc4314Rights("lrk"));
}

@Test
void shareeShouldBeAbleToCreateMailboxChildrenIntermediatePaths() throws Exception {
assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL));
session = mailboxManager.createSystemSession(USER_1);
MailboxPath mailboxPath = MailboxPath.inbox(session);
mailboxManager.createMailbox(mailboxPath, session);
mailboxManager.applyRightsCommand(mailboxPath,
MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_2))
.rights(MailboxACL.Rfc4314Rights.of(ImmutableList.of(MailboxACL.Right.Lookup,
MailboxACL.Right.Read, MailboxACL.Right.CreateMailbox)))
.asAddition(), session);

MailboxSession session2 = mailboxManager.createSystemSession(USER_2);
MailboxPath intermediatePath = MailboxPath.inbox(session)
.child("child", session2.getPathDelimiter());
MailboxPath childPath = intermediatePath.child("anotherkid", session2.getPathDelimiter());
mailboxManager.createMailbox(childPath, session2);

assertThat(mailboxManager.getMailbox(intermediatePath, session)
.getMailboxEntity().getACL().getEntries().get(MailboxACL.EntryKey.createUserEntryKey(USER_2)))
.isEqualTo(MailboxACL.Rfc4314Rights.fromSerializedRfc4314Rights("lrk"));
}

@Test
void shareeShouldBeAbleToDeleteMailbox() throws Exception {
assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL));
session = mailboxManager.createSystemSession(USER_1);
MailboxPath mailboxPath = MailboxPath.forUser(USER_1, "child");
mailboxManager.createMailbox(mailboxPath, session);
mailboxManager.applyRightsCommand(mailboxPath,
MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_2))
.rights(MailboxACL.Rfc4314Rights.of(ImmutableList.of(MailboxACL.Right.Lookup,
MailboxACL.Right.Read, MailboxACL.Right.DeleteMailbox)))
.asAddition(), session);

MailboxSession session2 = mailboxManager.createSystemSession(USER_2);
mailboxManager.deleteMailbox(mailboxPath, session2);

assertThatThrownBy(() -> mailboxManager.getMailbox(mailboxPath, session))
.isInstanceOf(MailboxNotFoundException.class);
}

@Test
void creatingMixedCaseINBOXShouldCreateItAsINBOX() throws Exception {
session = mailboxManager.createSystemSession(USER_1);
Expand Down Expand Up @@ -1199,6 +1289,32 @@ void searchShouldCombinePrivateAndDelegatedMailboxes() throws Exception {
.extracting(MailboxMetaData::getPath)
.containsOnly(inbox1, inbox2);
}

@Test
void searchShouldAllowListingAnotherUserMailbox() throws Exception {
assumeTrue(mailboxManager.hasCapability(MailboxCapabilities.ACL));
MailboxSession session1 = mailboxManager.createSystemSession(USER_1);
MailboxSession session2 = mailboxManager.createSystemSession(USER_2);
MailboxPath inbox1 = MailboxPath.inbox(session1);
MailboxPath inbox2 = MailboxPath.inbox(session2);
mailboxManager.createMailbox(inbox1, session1);
mailboxManager.createMailbox(inbox2, session2);
mailboxManager.setRights(inbox1,
MailboxACL.EMPTY.apply(MailboxACL.command()
.forUser(USER_2)
.rights(MailboxACL.Right.Read, MailboxACL.Right.Lookup)
.asAddition()),
session1);

MailboxQuery mailboxQuery = MailboxQuery.builder()
.userAndNamespaceFrom(inbox1)
.matchesAllMailboxNames()
.build();

assertThat(mailboxManager.search(mailboxQuery, session2).toStream())
.extracting(MailboxMetaData::getPath)
.containsExactly(inbox1);
}

@Test
void searchShouldAllowUserFiltering() throws Exception {
Expand Down Expand Up @@ -2093,7 +2209,7 @@ void user2ShouldNotBeAbleToDeleteUser1Mailbox() throws Exception {
mailboxManager.createMailbox(inbox, sessionUser1);

assertThatThrownBy(() -> mailboxManager.deleteMailbox(inbox, sessionUser2))
.isInstanceOf(MailboxNotFoundException.class);
.isInstanceOf(InsufficientRightsException.class);
}


Expand Down Expand Up @@ -3018,6 +3134,26 @@ void setRightsShouldThrowOnDeletedMailbox() throws Exception {

@Test
void setRightsByIdShouldThrowWhenNotOwner() throws Exception {
MailboxPath mailboxPath = MailboxPath.forUser(USER_2, "mailbox");
MailboxId id = mailboxManager.createMailbox(mailboxPath, session2).get();
mailboxManager.setRights(id, MailboxACL.EMPTY.apply(MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_1))
.rights(new MailboxACL.Rfc4314Rights(MailboxACL.Right.Lookup, MailboxACL.Right.Administer, MailboxACL.Right.Read))
.asAddition()), session2);

mailboxManager.setRights(id, MailboxACL.EMPTY.apply(
MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_1))
.rights(MailboxACL.FULL_RIGHTS)
.asAddition()), session);

assertThat(mailboxManager.getMailbox(mailboxPath, session2)
.getMailboxEntity().getACL().getEntries().get(MailboxACL.EntryKey.createUserEntryKey(USER_1)))
.isEqualTo(MailboxACL.Rfc4314Rights.fromSerializedRfc4314Rights("aeiklprstwx"));
}

@Test
void setRightsByIdShouldThrowWhenNotAdministrator() throws Exception {
MailboxId id = mailboxManager.createMailbox(MailboxPath.forUser(USER_2, "mailbox"), session2).get();
mailboxManager.setRights(id, MailboxACL.EMPTY.apply(MailboxACL.command()
.key(MailboxACL.EntryKey.createUserEntryKey(USER_1))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,12 @@ void isExpressionMatchShouldMatchFolderWhenMatching() {
}

@Test
void isExpressionMatchShouldReturnFalseWhenNameBeginsWithDelimiter() {
void isExpressionMatchShouldReturnTrueWhenNameBeginsWithDelimiter() {
PrefixedRegex testee = new PrefixedRegex(EMPTY_PREFIX, "mailbox", PATH_DELIMITER);

boolean actual = testee.isExpressionMatch(".mailbox");

assertThat(actual).isFalse();
assertThat(actual).isTrue();
}

@Test
Expand Down
Loading