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 <dlkh...@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-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org