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 019d93b44cbfd9052fa16085b7d8763c9d369763 Author: LanKhuat <[email protected]> AuthorDate: Tue Aug 18 10:41:33 2020 +0700 JAMES-3359 Mailbox/set update Rights reset --- .../james/jmap/rfc8621/contract/Fixture.scala | 2 + .../contract/MailboxSetMethodContract.scala | 191 ++++++++++++++++++++- .../org/apache/james/jmap/json/Serializer.scala | 2 + .../org/apache/james/jmap/mail/MailboxSet.scala | 14 +- .../james/jmap/method/MailboxSetMethod.scala | 31 +++- 5 files changed, 231 insertions(+), 9 deletions(-) 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/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala index 7022a76..71d2358 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala @@ -70,6 +70,8 @@ object Fixture { val _2_DOT_DOMAIN: Domain = Domain.of("do.main.tld") val BOB: Username = Username.fromLocalPartWithDomain("bob", DOMAIN) val ANDRE: Username = Username.fromLocalPartWithDomain("andre", DOMAIN) + val CEDRIC: Username = Username.fromLocalPartWithDomain("cedric", DOMAIN) + val DAVID: Username = Username.fromLocalPartWithDomain("david", DOMAIN) val ALICE: Username = Username.fromLocalPartWithDomain("alice", _2_DOT_DOMAIN) val BOB_PASSWORD: String = "bobpassword" val ANDRE_PASSWORD: String = "andrepassword" 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 a3bc8ca..bf52e3a 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 @@ -31,7 +31,7 @@ import org.apache.http.HttpStatus.SC_OK import org.apache.james.GuiceJamesServer import org.apache.james.jmap.draft.MessageIdProbe import org.apache.james.jmap.http.UserCredential -import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder} +import org.apache.james.jmap.rfc8621.contract.Fixture._ import org.apache.james.mailbox.MessageManager.AppendCommand import org.apache.james.mailbox.model.MailboxACL.Right import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath} @@ -3220,4 +3220,193 @@ trait MailboxSetMethodContract { //Should be replaced with JMAP message query when it is available assertThat(server.getProbe(classOf[MessageIdProbe]).getMessages(messageId.getMessageId, BOB)).isNotEmpty } + + @Test + def updateShouldAllowSettingRights(server: GuiceJamesServer): Unit = { + val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox")) + 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"] + | } + | }], + | "notFound": [] + | }, "c2"] + | ] + |}""".stripMargin) + } + + @Test + def updateShouldAllowResettingRights(server: GuiceJamesServer): Unit = { + val path = MailboxPath.forUser(BOB, "mailbox") + val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path) + + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(path, ANDRE.asString(), new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Administer)) + server.getProbe(classOf[ACLProbeImpl]) + .replaceRights(path, CEDRIC.asString(), new MailboxACL.Rfc4314Rights(Right.Read, 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"], + | "${DAVID.asString()}":["r", "l", "w"] + | } + | } + | } + | }, + | "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", "r", "w"] + | } + | }], + | "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 212e840..2140562 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 @@ -391,4 +391,6 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) { def deserializeMailboxGetRequest(input: JsValue): JsResult[MailboxGetRequest] = Json.fromJson[MailboxGetRequest](input) def deserializeMailboxSetRequest(input: JsValue): JsResult[MailboxSetRequest] = Json.fromJson[MailboxSetRequest](input) + + def deserializeRights(input: JsValue): JsResult[Rights] = Json.fromJson[Rights](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 b34af2c..739fa5c 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.jmap.json.Serializer import org.apache.james.jmap.mail.MailboxName.MailboxName import org.apache.james.jmap.mail.MailboxPatchObject.MailboxPatchObjectKey import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId} @@ -33,7 +34,7 @@ import org.apache.james.jmap.model.AccountId import org.apache.james.jmap.model.State.State import org.apache.james.mailbox.Role import org.apache.james.mailbox.model.MailboxId -import play.api.libs.json.{JsObject, JsString, JsValue} +import play.api.libs.json._ case class MailboxSetRequest(accountId: AccountId, ifInState: Option[State], @@ -65,9 +66,10 @@ object MailboxPatchObject { } case class MailboxPatchObject(value: Map[String, JsValue]) { - def updates: Iterable[Either[PatchUpdateValidationException, Update]] = value.map({ + def updates(serializer: Serializer): Iterable[Either[PatchUpdateValidationException, Update]] = value.map({ case (property, newValue) => property match { case "/name" => NameUpdate.parse(newValue) + case "/sharedWith" => SharedWithResetUpdate.parse(newValue, serializer) case property => val refinedKey: Either[String, MailboxPatchObjectKey] = refineV(property) refinedKey.fold[Either[PatchUpdateValidationException, Update]]( @@ -134,8 +136,16 @@ object NameUpdate { } } +object SharedWithResetUpdate { + def parse(newValue: JsValue, serializer: Serializer): Either[PatchUpdateValidationException, Update] = serializer.deserializeRights(input = newValue) match { + case JsSuccess(value, _) => scala.Right(SharedWithResetUpdate(value)) + case JsError(errors) => Left(InvalidUpdateException("/sharedWith", 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 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 d7bd881..24bebd5 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,10 +23,10 @@ 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, MailboxCreationRequest, MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, PatchUpdateValidationException, Properties, RemoveEmailsOnDestroy, SetErrorDescription, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedException} +import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateException, IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, PatchUpdateValidationException, Properties, RemoveEmailsOnDestroy, SetErrorDescription, SharedWithResetUpdate, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedException} 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} +import org.apache.james.jmap.model._ import org.apache.james.jmap.routes.ProcessingContext import org.apache.james.mailbox.MailboxManager.RenameOption import org.apache.james.mailbox.exception.{InsufficientRightsException, MailboxExistsException, MailboxNameException, MailboxNotFoundException} @@ -180,22 +180,28 @@ class MailboxSetMethod @Inject()(serializer: Serializer, private def updateMailbox(mailboxSession: MailboxSession, maiboxId: MailboxId, patch: MailboxPatchObject): SMono[UpdateResult] = { - val maybeParseException: Option[PatchUpdateValidationException] = patch.updates + val updates = patch.updates(serializer) + val maybeParseException: Option[PatchUpdateValidationException] = updates .flatMap(x => x match { case Left(e) => Some(e) case _ => None }).headOption - val maybeNameUpdate: Option[NameUpdate] = patch.updates + val maybeNameUpdate: Option[NameUpdate] = updates .flatMap(x => x match { case Right(NameUpdate(newName)) => Some(NameUpdate(newName)) case _ => None }).headOption - def renameMailbox: SMono[UpdateResult] = updateMailboxPath(maiboxId, maybeNameUpdate, mailboxSession) + val maybeSharedWithResetUpdate: Option[SharedWithResetUpdate] = updates + .flatMap(x => x match { + case Right(SharedWithResetUpdate(rights)) => Some(SharedWithResetUpdate(rights)) + case _ => None + }).headOption maybeParseException.map(e => SMono.raiseError[UpdateResult](e)) - .getOrElse(renameMailbox) + .getOrElse(updateMailboxPath(maiboxId, maybeNameUpdate, mailboxSession) + .`then`(updateMailboxRights(maiboxId, maybeSharedWithResetUpdate, mailboxSession))) } private def updateMailboxPath(mailboxId: MailboxId, maybeNameUpdate: Option[NameUpdate], mailboxSession: MailboxSession): SMono[UpdateResult] = { @@ -216,6 +222,19 @@ class MailboxSetMethod @Inject()(serializer: Serializer, .getOrElse(SMono.just[UpdateResult](UpdateSuccess(mailboxId))) } + private def updateMailboxRights(mailboxId: MailboxId, + maybeSharedWithResetUpdate: Option[SharedWithResetUpdate], + mailboxSession: MailboxSession): SMono[UpdateResult] = { + 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))) + } + private def computeMailboxPath(mailbox: MessageManager, nameUpdate: NameUpdate, mailboxSession: MailboxSession): MailboxPath = { val originalPath: MailboxPath = mailbox.getMailboxPath val maybeParentPath: Option[MailboxPath] = originalPath.getHierarchyLevels(mailboxSession.getPathDelimiter) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
