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