You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/08/25 02:19:52 UTC

[james-project] 01/07: JAMES-3359 Mailbox/set rights partial update

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

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

commit 81609975976d55fe5f023543380cf27a0fc945fd
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Aug 19 09:56:31 2020 +0700

    JAMES-3359 Mailbox/set rights partial update
---
 .../org/apache/james/mailbox/RightManager.java     |  2 +
 .../james/mailbox/store/StoreMailboxManager.java   |  5 ++
 .../james/mailbox/store/StoreRightManager.java     |  9 +++
 .../contract/MailboxSetMethodContract.scala        | 94 ++++++++++++++++++++++
 .../org/apache/james/jmap/json/Serializer.scala    |  7 +-
 .../org/apache/james/jmap/mail/MailboxSet.scala    | 30 ++++++-
 .../james/jmap/method/MailboxSetMethod.scala       | 32 ++++++--
 7 files changed, 169 insertions(+), 10 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/RightManager.java b/mailbox/api/src/main/java/org/apache/james/mailbox/RightManager.java
index 4e5c705..84eda2b 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/RightManager.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/RightManager.java
@@ -135,6 +135,8 @@ public interface RightManager {
      */
     void applyRightsCommand(MailboxPath mailboxPath, MailboxACL.ACLCommand mailboxACLCommand, MailboxSession session) throws MailboxException;
 
+    void applyRightsCommand(MailboxId mailboxId, MailboxACL.ACLCommand mailboxACLCommand, MailboxSession session) throws MailboxException;
+
     /**
      * Reset the Mailbox ACL of the designated mailbox.
      *
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
index 091e40b..faf4d50 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/StoreMailboxManager.java
@@ -797,6 +797,11 @@ public class StoreMailboxManager implements MailboxManager {
     }
 
     @Override
+    public void applyRightsCommand(MailboxId mailboxId, MailboxACL.ACLCommand mailboxACLCommand, MailboxSession session) throws MailboxException {
+        storeRightManager.applyRightsCommand(mailboxId, mailboxACLCommand, session);
+    }
+
+    @Override
     public void setRights(MailboxPath mailboxPath, MailboxACL mailboxACL, MailboxSession session) throws MailboxException {
         storeRightManager.setRights(mailboxPath, mailboxACL, session);
     }
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 afd7b90..2845ed8 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
@@ -163,6 +163,15 @@ public class StoreRightManager implements RightManager {
             }).sneakyThrow()));
     }
 
+    @Override
+    public void applyRightsCommand(MailboxId mailboxId, ACLCommand mailboxACLCommand, MailboxSession session) throws MailboxException {
+        MailboxMapper mapper = mailboxSessionMapperFactory.getMailboxMapper(session);
+        Mailbox mailbox = blockOptional(mapper.findMailboxById(mailboxId))
+            .orElseThrow(() -> new MailboxNotFoundException(mailboxId));
+
+        applyRightsCommand(mailbox.generateAssociatedPath(), mailboxACLCommand, session);
+    }
+
     private void assertSharesBelongsToUserDomain(Username user, ACLCommand mailboxACLCommand) throws DifferentDomainException {
         assertSharesBelongsToUserDomain(user, ImmutableMap.of(mailboxACLCommand.getEntryKey(), mailboxACLCommand.getRights()));
     }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
index 3d197e8..b18f8db 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
@@ -5073,4 +5073,98 @@ trait MailboxSetMethodContract {
          |  ]
          |}""".stripMargin)
   }
+
+  @Test
+  def updateShouldAllowPerRightsSetting(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.forUser(BOB, "mailbox")
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(path, DAVID.asString(), new MailboxACL.Rfc4314Rights(Right.Lookup))
+
+    val request =
+      s"""
+         |{
+         |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares" ],
+         |   "methodCalls": [
+         |       [
+         |           "Mailbox/set",
+         |           {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "update": {
+         |                    "${mailboxId.serialize()}": {
+         |                      "/sharedWith/${ANDRE.asString()}": ["r", "l"]
+         |                    }
+         |                }
+         |           },
+         |    "c1"
+         |       ],
+         |       ["Mailbox/get",
+         |         {
+         |           "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |           "ids": ["${mailboxId.serialize()}"]
+         |          },
+         |       "c2"]
+         |   ]
+         |}
+         |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |  "sessionState": "75128aab4b1b",
+         |  "methodResponses": [
+         |    ["Mailbox/set", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "newState": "000001",
+         |      "updated": {
+         |        "${mailboxId.serialize()}": {}
+         |      }
+         |    }, "c1"],
+         |    ["Mailbox/get", {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "state": "000001",
+         |      "list": [{
+         |        "id": "${mailboxId.serialize()}",
+         |        "name": "mailbox",
+         |        "sortOrder": 1000,
+         |        "totalEmails": 0,
+         |        "unreadEmails": 0,
+         |        "totalThreads": 0,
+         |        "unreadThreads": 0,
+         |        "myRights": {
+         |          "mayReadItems": true,
+         |          "mayAddItems": true,
+         |          "mayRemoveItems": true,
+         |          "maySetSeen": true,
+         |          "maySetKeywords": true,
+         |          "mayCreateChild": true,
+         |          "mayRename": true,
+         |          "mayDelete": true,
+         |          "maySubmit": true
+         |        },
+         |        "isSubscribed": false,
+         |        "namespace": "Personal",
+         |        "rights": {
+         |          "${ANDRE.asString()}": ["l", "r"],
+         |          "${DAVID.asString()}": ["l"]
+         |        }
+         |      }],
+         |      "notFound": []
+         |    }, "c2"]
+         |  ]
+         |}""".stripMargin)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
index 2f8a2a8..ae37616 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
@@ -27,12 +27,13 @@ import eu.timepit.refined.auto._
 import javax.inject.Inject
 import org.apache.james.core.{Domain, Username}
 import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId}
-import org.apache.james.jmap.mail.{DelegatedNamespace, Ids, IsSubscribed, Mailbox, MailboxCreationRequest, MailboxCreationResponse, MailboxGetRequest, MailboxGetResponse, MailboxNamespace, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, MayAddItems, MayCreateChild, MayDelete, MayReadItems, MayRemoveItems, MayRename, MaySetKeywords, MaySetSeen, MaySubmit, NotFound, PersonalNamespace, Properties, Quota, QuotaId, QuotaRoot, Q [...]
+import org.apache.james.jmap.mail.{DelegatedNamespace, Ids, IsSubscribed, Mailbox, MailboxCreationRequest, MailboxCreationResponse, MailboxGetRequest, MailboxGetResponse, MailboxNamespace, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, MayAddItems, MayCreateChild, MayDelete, MayReadItems, MayRemoveItems, MayRename, MaySetKeywords, MaySetSeen, MaySubmit, NotFound, PersonalNamespace, Properties, Quota, QuotaId, QuotaRoot, Q [...]
 import org.apache.james.jmap.model
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodCallId, MethodName}
 import org.apache.james.jmap.model.{Account, Invocation, Session, _}
 import org.apache.james.mailbox.Role
+import org.apache.james.mailbox.model.MailboxACL.{Right => JavaRight}
 import org.apache.james.mailbox.model.{MailboxACL, MailboxId}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
@@ -189,6 +190,8 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
       }
     case _ => JsError("Right must be represented as a String")
   }
+  private implicit val mailboxJavaRightReads: Reads[JavaRight] = value => rightRead.reads(value).map(right => right.toMailboxRight)
+  private implicit val mailboxRfc4314RightsReads: Reads[Rfc4314Rights] = Json.valueReads[Rfc4314Rights]
   private implicit val rightsWrites: Writes[Rights] = Json.valueWrites[Rights]
 
   private implicit val mapRightsReads: Reads[Map[Username, Seq[Right]]] = _.validate[Map[String, Seq[Right]]]
@@ -403,4 +406,6 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
   def deserializeMailboxSetRequest(input: JsValue): JsResult[MailboxSetRequest] = Json.fromJson[MailboxSetRequest](input)
 
   def deserializeRights(input: JsValue): JsResult[Rights] = Json.fromJson[Rights](input)
+
+  def deserializeRfc4314Rights(input: JsValue): JsResult[Rfc4314Rights] = Json.fromJson[Rfc4314Rights](input)
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
index 4fc8b92..e5e5525 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
@@ -26,6 +26,7 @@ import eu.timepit.refined.boolean.And
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import eu.timepit.refined.string.StartsWith
+import org.apache.james.core.Username
 import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.mail.MailboxName.MailboxName
 import org.apache.james.jmap.mail.MailboxPatchObject.MailboxPatchObjectKey
@@ -34,8 +35,8 @@ import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.State.State
 import org.apache.james.jmap.model.{AccountId, CapabilityIdentifier}
 import org.apache.james.mailbox.Role
-import org.apache.james.mailbox.model.MailboxId
 import play.api.libs.json.{JsBoolean, JsError, JsNull, JsObject, JsString, JsSuccess, JsValue}
+import org.apache.james.mailbox.model.{MailboxId, MailboxACL => JavaMailboxACL}
 
 case class MailboxSetRequest(accountId: AccountId,
                              ifInState: Option[State],
@@ -74,6 +75,7 @@ object MailboxPatchObject {
   val unreadEmailsProperty: MailboxPatchObjectKey = "/unreadEmails"
   val totalEmailsProperty: MailboxPatchObjectKey = "/totalEmails"
   val myRightsProperty: MailboxPatchObjectKey = "/myRights"
+  val sharedWithPrefix = "/sharedWith/"
 }
 
 case class MailboxPatchObject(value: Map[String, JsValue]) {
@@ -91,6 +93,7 @@ case class MailboxPatchObject(value: Map[String, JsValue]) {
       case "/totalEmails" => Left(ServerSetPropertyException(MailboxPatchObject.totalEmailsProperty))
       case "/myRights" => Left(ServerSetPropertyException(MailboxPatchObject.myRightsProperty))
       case "/isSubscribed" => IsSubscribedUpdate.parse(newValue)
+      case property: String if property.startsWith(MailboxPatchObject.sharedWithPrefix) => SharedWithPartialUpdate.parse(newValue, property, serializer)
       case property =>
         val refinedKey: Either[String, MailboxPatchObjectKey] = refineV(property)
         refinedKey.fold[Either[PatchUpdateValidationException, Update]](
@@ -186,10 +189,35 @@ object IsSubscribedUpdate {
   }
 }
 
+object SharedWithPartialUpdate {
+  def parse(newValue: JsValue, property: String, serializer: Serializer): Either[PatchUpdateValidationException, Update] =
+    parseUsername(property)
+      .flatMap(username => parseRights(newValue, property, serializer)
+        .map(rights => SharedWithPartialUpdate(username, rights)))
+
+  def parseUsername(property: String): Either[PatchUpdateValidationException, Username] = try {
+    scala.Right(Username.of(property.substring(MailboxPatchObject.sharedWithPrefix.length)))
+  } catch {
+    case e: Exception => Left(InvalidPropertyException(property, e.getMessage))
+  }
+
+  def parseRights(newValue: JsValue, property: String, serializer: Serializer): Either[PatchUpdateValidationException, Rfc4314Rights] = serializer.deserializeRfc4314Rights(newValue) match {
+    case JsSuccess(rights, _) => scala.Right(rights)
+    case JsError(errors) =>
+      val refinedKey: Either[String, MailboxPatchObjectKey] = refineV(property)
+      refinedKey.fold(
+        refinedError => Left(InvalidPropertyException(property = property, cause = s"Invalid property specified in a patch object: $refinedError")),
+        refinedProperty => Left(InvalidUpdateException(refinedProperty, s"Specified value do not match the expected JSON format: $errors")))
+  }
+}
+
 sealed trait Update
 case class NameUpdate(newName: String) extends Update
 case class SharedWithResetUpdate(rights: Rights) extends Update
 case class IsSubscribedUpdate(isSubscribed: Option[IsSubscribed]) extends Update
+case class SharedWithPartialUpdate(username: Username, rights: Rfc4314Rights) extends Update {
+  def asACLCommand(): JavaMailboxACL.ACLCommand = JavaMailboxACL.command().forUser(username).rights(rights.asJava).asReplacement()
+}
 
 class PatchUpdateValidationException() extends IllegalArgumentException
 case class UnsupportedPropertyUpdatedException(property: MailboxPatchObjectKey) extends PatchUpdateValidationException
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
index 0ecc29a..2f523aa 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
@@ -23,7 +23,7 @@ import eu.timepit.refined.auto._
 import javax.inject.Inject
 import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId}
-import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateException, IsSubscribed, IsSubscribedUpdate, MailboxCreationRequest, MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, PatchUpdateValidationException, Properties, RemoveEmailsOnDestroy, ServerSetPropertyException, SetErrorDescription, SharedWithResetUpdate, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadT [...]
+import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateException, IsSubscribed, IsSubscribedUpdate, MailboxCreationRequest, MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, PatchUpdateValidationException, Properties, RemoveEmailsOnDestroy, ServerSetPropertyException, SetErrorDescription, SharedWithPartialUpdate, SharedWithResetUpdate, SortOrder, TotalEmails, TotalThrea [...]
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.model.{ClientId, Id, Invocation, ServerId, State}
@@ -195,9 +195,14 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
         case _ => None
       }).headOption
 
+    val partialRightsUpdates: Seq[SharedWithPartialUpdate] = updates.flatMap(x => x match {
+      case Right(SharedWithPartialUpdate(username, rights)) => Some(SharedWithPartialUpdate(username, rights))
+      case _ => None
+    }).toSeq
+
     maybeParseException.map(e => SMono.raiseError[UpdateResult](e))
       .getOrElse(updateMailboxPath(maiboxId, maybeNameUpdate, mailboxSession)
-        .`then`(updateMailboxRights(maiboxId, maybeSharedWithResetUpdate, mailboxSession))
+        .`then`(updateMailboxRights(maiboxId, maybeSharedWithResetUpdate, partialRightsUpdates, mailboxSession))
         .`then`(updateSubscription(maiboxId, maybeIsSubscribedUpdate, mailboxSession)))
   }
 
@@ -239,15 +244,26 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
 
   private def updateMailboxRights(mailboxId: MailboxId,
                                   maybeSharedWithResetUpdate: Option[SharedWithResetUpdate],
+                                  partialUpdates: Seq[SharedWithPartialUpdate],
                                   mailboxSession: MailboxSession): SMono[UpdateResult] = {
-   maybeSharedWithResetUpdate.map(sharedWithResetUpdate => {
+
+    val resetOperation: SMono[Unit] = maybeSharedWithResetUpdate.map(sharedWithResetUpdate => {
       SMono.fromCallable(() => {
         mailboxManager.setRights(mailboxId, sharedWithResetUpdate.rights.toMailboxAcl.asJava, mailboxSession)
-      }).`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
-        .subscribeOn(Schedulers.elastic())
-    })
-      // No updated properties passed. Noop.
-      .getOrElse(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
+      }).`then`()
+    }).getOrElse(SMono.empty)
+
+    val partialUpdatesOperation: SMono[Unit] = SFlux.fromIterable(partialUpdates)
+      .flatMap(partialUpdate => SMono.fromCallable(() => {
+        mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)
+      }))
+      .`then`()
+
+    SFlux.merge(Seq(resetOperation, partialUpdatesOperation))
+      .`then`()
+      .`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
+      .subscribeOn(Schedulers.elastic())
+
   }
 
   private def computeMailboxPath(mailbox: MessageManager, nameUpdate: NameUpdate, mailboxSession: MailboxSession): MailboxPath = {


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org