You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/01/18 05:05:56 UTC

[james-project] branch master updated: JAMES-3756 DelegatedAccount/set destroy (#1398)

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


The following commit(s) were added to refs/heads/master by this push:
     new f748329c48 JAMES-3756 DelegatedAccount/set destroy (#1398)
f748329c48 is described below

commit f748329c48a32b9a4544ae2454b5bde01c97862b
Author: Trần Hồng Quân <55...@users.noreply.github.com>
AuthorDate: Wed Jan 18 12:05:50 2023 +0700

    JAMES-3756 DelegatedAccount/set destroy (#1398)
---
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   2 +
 .../DistributedDelegatedAccountSetMethodTest.java  |  57 +++
 .../contract/DelegatedAccountSetContract.scala     | 385 +++++++++++++++++++++
 .../rfc8621/contract/probe/DelegationProbe.scala   |   6 +-
 .../MemoryDelegatedAccountSetMethodTest.java}      |  49 ++-
 .../apache/james/jmap/delegation/DelegateGet.scala |  21 +-
 .../jmap/delegation/DelegatedAccountSet.scala      |  13 +
 .../james/jmap/json/DelegationSerializer.scala     |   9 +-
 .../method/DelegatedAccountDeletePerformer.scala   |  77 +++++
 .../jmap/method/DelegatedAccountSetMethod.scala    |  68 ++++
 10 files changed, 654 insertions(+), 33 deletions(-)

diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index bdc9c39228..8809901963 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -50,6 +50,7 @@ import org.apache.james.jmap.method.CoreEchoMethod;
 import org.apache.james.jmap.method.DelegateGetMethod;
 import org.apache.james.jmap.method.DelegateSetMethod;
 import org.apache.james.jmap.method.DelegatedAccountGetMethod;
+import org.apache.james.jmap.method.DelegatedAccountSetMethod;
 import org.apache.james.jmap.method.EmailChangesMethod;
 import org.apache.james.jmap.method.EmailGetMethod;
 import org.apache.james.jmap.method.EmailImportMethod;
@@ -158,6 +159,7 @@ public class RFC8621MethodsModule extends AbstractModule {
         methods.addBinding().to(DelegatedAccountGetMethod.class);
         methods.addBinding().to(DelegateGetMethod.class);
         methods.addBinding().to(DelegateSetMethod.class);
+        methods.addBinding().to(DelegatedAccountSetMethod.class);
 
         Multibinder<JMAPRoutes> routes = Multibinder.newSetBinder(binder(), JMAPRoutes.class);
         routes.addBinding().to(SessionRoutes.class);
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedDelegatedAccountSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedDelegatedAccountSetMethodTest.java
new file mode 100644
index 0000000000..caa9c36e6d
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedDelegatedAccountSetMethodTest.java
@@ -0,0 +1,57 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerOpenSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.SearchConfiguration;
+import org.apache.james.jmap.rfc8621.contract.DelegatedAccountSetContract;
+import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbeModule;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class DistributedDelegatedAccountSetMethodTest implements DelegatedAccountSetContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+        CassandraRabbitMQJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .blobStore(BlobStoreConfiguration.builder()
+                .s3()
+                .disableCache()
+                .deduplication()
+                .noCryptoConfig())
+            .searchConfiguration(SearchConfiguration.openSearch())
+            .build())
+        .extension(new DockerOpenSearchExtension())
+        .extension(new CassandraExtension())
+        .extension(new RabbitMQExtension())
+        .extension(new AwsS3BlobStoreExtension())
+        .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule(), new DelegationProbeModule()))
+        .build();
+}
\ No newline at end of file
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/DelegatedAccountSetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DelegatedAccountSetContract.scala
new file mode 100644
index 0000000000..df7a0678e6
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DelegatedAccountSetContract.scala
@@ -0,0 +1,385 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.contract
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+import io.restassured.http.ContentType.JSON
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.delegation.DelegationId
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.DelegatedAccountSetContract.BOB_ACCOUNT_ID
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_ACCOUNT_ID, ANDRE_PASSWORD, BOB, BOB_PASSWORD, CEDRIC, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbe
+import org.apache.james.utils.DataProbeImpl
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+import scala.jdk.CollectionConverters._
+
+object DelegatedAccountSetContract {
+  val BOB_ACCOUNT_ID: String = Fixture.ACCOUNT_ID
+}
+
+trait DelegatedAccountSetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addUser(BOB.asString, BOB_PASSWORD)
+      .addUser(ANDRE.asString(), ANDRE_PASSWORD)
+      .addUser(CEDRIC.asString(), "secret")
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def delegatedAccountDestroyShouldSucceed(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DelegationProbe])
+      .addAuthorizedUser(ANDRE, BOB)
+    val andreToBobDelegationId = DelegationId.from(ANDRE, BOB).serialize
+
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:apache:james:params:jmap:delegation"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$BOB_ACCOUNT_ID",
+         |				"destroy": ["$andreToBobDelegationId"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "DelegatedAccount/set",
+           |            {
+           |                "accountId": "$BOB_ACCOUNT_ID",
+           |                "newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |                "destroyed": ["$andreToBobDelegationId"]
+           |            },
+           |            "0"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+
+    assertThat(server.getProbe(classOf[DelegationProbe]).getDelegatedUsers(BOB).asJavaCollection)
+      .isEmpty()
+  }
+
+  @Test
+  def mixedCaseShouldDestroyOnlyRequestedEntry(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DelegationProbe])
+      .addAuthorizedUser(ANDRE, BOB)
+    server.getProbe(classOf[DelegationProbe])
+      .addAuthorizedUser(CEDRIC, BOB)
+    val andreToBobDelegationId = DelegationId.from(ANDRE, BOB).serialize
+
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:apache:james:params:jmap:delegation"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$BOB_ACCOUNT_ID",
+         |				"destroy": ["$andreToBobDelegationId"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "DelegatedAccount/set",
+           |            {
+           |                "accountId": "$BOB_ACCOUNT_ID",
+           |                "newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |                "destroyed": ["$andreToBobDelegationId"]
+           |            },
+           |            "0"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+
+    assertThat(server.getProbe(classOf[DelegationProbe]).getDelegatedUsers(BOB).asJavaCollection)
+      .containsExactly(CEDRIC)
+  }
+
+  @Test
+  def delegatedAccountDestroyShouldFailWhenMissingDelegationCapability(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DelegationProbe])
+      .addAuthorizedUser(ANDRE, BOB)
+    val delegationId = DelegationId.from(ANDRE, BOB).serialize
+
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$BOB_ACCOUNT_ID",
+         |				"destroy": ["$delegationId"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "${SESSION_STATE.value}",
+           |	"methodResponses": [
+           |		[
+           |			"error",
+           |			{
+           |				"type": "unknownMethod",
+           |				"description": "Missing capability(ies): urn:apache:james:params:jmap:delegation"
+           |			},
+           |			"0"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def delegatedAccountDestroyShouldBeIdempotent(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DelegationProbe])
+      .addAuthorizedUser(ANDRE, BOB)
+    val andreToBobDelegationId = DelegationId.from(ANDRE, BOB).serialize
+
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:apache:james:params:jmap:delegation"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$BOB_ACCOUNT_ID",
+         |				"destroy": ["$andreToBobDelegationId", "$andreToBobDelegationId"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "DelegatedAccount/set",
+           |            {
+           |                "accountId": "$BOB_ACCOUNT_ID",
+           |                "newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |                "destroyed": ["$andreToBobDelegationId", "$andreToBobDelegationId"]
+           |            },
+           |            "0"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def shouldReturnNotFoundWhenTryToAccessNonDelegatedAccount(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:apache:james:params:jmap:delegation"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$ANDRE_ACCOUNT_ID",
+         |				"destroy": ["any"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"type": "accountNotFound"
+           |}""".stripMargin)
+  }
+
+  @Test
+  def bobCanOnlyManageHisPrimaryAccountSetting(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DelegationProbe]).addAuthorizedUser(ANDRE, BOB)
+
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:apache:james:params:jmap:delegation"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$ANDRE_ACCOUNT_ID",
+         |				"destroy": ["any"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0]")
+      .isEqualTo(
+        s"""[
+           |	"error",
+           |	{
+           |		"type": "forbidden",
+           |		"description": "Access to other accounts settings is forbidden"
+           |	},
+           |	"0"
+           |]""".stripMargin)
+  }
+
+
+  @Test
+  def destroyShouldFailWhenInvalidId(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:apache:james:params:jmap:delegation"],
+         |	"methodCalls": [
+         |		[
+         |			"DelegatedAccount/set", {
+         |				"accountId": "$BOB_ACCOUNT_ID",
+         |				"destroy": ["invalid"]
+         |			}, "0"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0]")
+      .isEqualTo(
+        s"""[
+           |	"DelegatedAccount/set",
+           |	{
+           |		"accountId": "$BOB_ACCOUNT_ID",
+           |		"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |		"notDestroyed": {
+           |			"invalid": {
+           |				"type": "invalidArguments",
+           |				"description": "invalid is not a DelegationId: Invalid UUID string: invalid"
+           |			}
+           |		}
+           |	},
+           |	"0"
+           |]""".stripMargin)
+  }
+}
\ No newline at end of file
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/probe/DelegationProbe.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/DelegationProbe.scala
index cbe99df8a5..2d2aa7ba1d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/DelegationProbe.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/DelegationProbe.scala
@@ -21,7 +21,6 @@ package org.apache.james.jmap.rfc8621.contract.probe
 
 import com.google.inject.AbstractModule
 import com.google.inject.multibindings.Multibinder
-
 import javax.inject.Inject
 import org.apache.james.core.Username
 import org.apache.james.jmap.core.AccountId
@@ -42,6 +41,11 @@ class DelegationProbe @Inject()(delegationStore: DelegationStore) extends GuiceP
       .collectSeq()
       .block()
 
+  def getDelegatedUsers(baseUser: Username): Seq[Username] =
+    SFlux.fromPublisher(delegationStore.delegatedUsers(baseUser))
+      .collectSeq()
+      .block()
+
   def addAuthorizedUser(baseUser: Username, userWithAccess: Username): AccountId =
     SMono.fromPublisher(delegationStore.addAuthorizedUser(baseUser, userWithAccess))
       .`then`(SMono.just(AccountId.from(userWithAccess).toOption.get))
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/probe/DelegationProbe.scala b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryDelegatedAccountSetMethodTest.java
similarity index 50%
copy from server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/DelegationProbe.scala
copy to server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryDelegatedAccountSetMethodTest.java
index cbe99df8a5..0f15e36429 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/probe/DelegationProbe.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryDelegatedAccountSetMethodTest.java
@@ -17,33 +17,28 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.rfc8621.contract.probe
+package org.apache.james.jmap.rfc8621.memory;
 
-import com.google.inject.AbstractModule
-import com.google.inject.multibindings.Multibinder
+import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;
 
-import javax.inject.Inject
-import org.apache.james.core.Username
-import org.apache.james.jmap.core.AccountId
-import org.apache.james.user.api.DelegationStore
-import org.apache.james.utils.GuiceProbe
-import reactor.core.scala.publisher.{SFlux, SMono}
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.MemoryJamesConfiguration;
+import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.jmap.rfc8621.contract.DelegatedAccountSetContract;
+import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbeModule;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
 
-class DelegationProbeModule extends AbstractModule {
-  override def configure(): Unit =
-    Multibinder.newSetBinder(binder(), classOf[GuiceProbe])
-      .addBinding()
-      .to(classOf[DelegationProbe])
-}
-
-class DelegationProbe @Inject()(delegationStore: DelegationStore) extends GuiceProbe {
-  def getAuthorizedUsers(baseUser: Username): Seq[Username] =
-    SFlux.fromPublisher(delegationStore.authorizedUsers(baseUser))
-      .collectSeq()
-      .block()
-
-  def addAuthorizedUser(baseUser: Username, userWithAccess: Username): AccountId =
-    SMono.fromPublisher(delegationStore.addAuthorizedUser(baseUser, userWithAccess))
-      .`then`(SMono.just(AccountId.from(userWithAccess).toOption.get))
-      .block()
-}
+public class MemoryDelegatedAccountSetMethodTest implements DelegatedAccountSetContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir ->
+        MemoryJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .usersRepository(DEFAULT)
+            .build())
+        .server(configuration -> MemoryJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule(), new DelegationProbeModule()))
+        .build();
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegateGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegateGet.scala
index b4ed338cd4..ccaf60af14 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegateGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegateGet.scala
@@ -19,11 +19,15 @@
 
 package org.apache.james.jmap.delegation
 
-import org.apache.james.jmap.core.{AccountId, Properties}
-import org.apache.james.jmap.core.Id.Id
-import org.apache.james.jmap.method.WithAccountId
+import java.util.UUID
+
 import eu.timepit.refined.auto._
 import org.apache.james.core.Username
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.{AccountId, Properties}
+import org.apache.james.jmap.method.WithAccountId
+
+import scala.util.Try
 
 object DelegateGet {
   val allProperties: Properties = Properties("id", "username")
@@ -31,7 +35,16 @@ object DelegateGet {
   def propertiesFiltered(requestedProperties: Properties): Properties = idProperty ++ requestedProperties
 }
 
-case class UnparsedDelegateId(id: Id)
+case class UnparsedDelegateId(id: Id) {
+  def parse: Either[IllegalArgumentException, DelegationId] =
+    Try(UUID.fromString(id.value))
+      .map(DelegationId(_))
+      .toEither
+      .left.map({
+      case e: IllegalArgumentException => e
+      case e => new IllegalArgumentException(e)
+    })
+}
 
 case class DelegateIds(value: List[UnparsedDelegateId])
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegatedAccountSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegatedAccountSet.scala
new file mode 100644
index 0000000000..330162aac1
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/delegation/DelegatedAccountSet.scala
@@ -0,0 +1,13 @@
+package org.apache.james.jmap.delegation
+
+import org.apache.james.jmap.core.{AccountId, SetError, UuidState}
+import org.apache.james.jmap.method.WithAccountId
+
+case class DelegatedAccountSetRequest(accountId: AccountId,
+                                      destroy: Option[Seq[UnparsedDelegateId]]) extends WithAccountId
+
+case class DelegatedAccountSetResponse(accountId: AccountId,
+                                       oldState: Option[UuidState],
+                                       newState: UuidState,
+                                       destroyed: Option[Seq[DelegationId]],
+                                       notDestroyed: Option[Map[UnparsedDelegateId, SetError]])
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/DelegationSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/DelegationSerializer.scala
index 505f2bdea2..1539630c66 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/DelegationSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/DelegationSerializer.scala
@@ -24,7 +24,7 @@ import eu.timepit.refined.refineV
 import org.apache.james.core.Username
 import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.{Properties, SetError, UuidState}
-import org.apache.james.jmap.delegation.{Delegate, DelegateCreationId, DelegateCreationRequest, DelegateCreationResponse, DelegateGet, DelegateGetRequest, DelegateGetResponse, DelegateIds, DelegateNotFound, DelegateSetRequest, DelegateSetResponse, DelegatedAccountGet, DelegatedAccountGetRequest, DelegatedAccountGetResponse, DelegatedAccountNotFound, DelegationId, UnparsedDelegateId}
+import org.apache.james.jmap.delegation.{Delegate, DelegateCreationId, DelegateCreationRequest, DelegateCreationResponse, DelegateGet, DelegateGetRequest, DelegateGetResponse, DelegateIds, DelegateNotFound, DelegateSetRequest, DelegateSetResponse, DelegatedAccountGet, DelegatedAccountGetRequest, DelegatedAccountGetResponse, DelegatedAccountNotFound, DelegatedAccountSetRequest, DelegatedAccountSetResponse, DelegationId, UnparsedDelegateId}
 import play.api.libs.json.{Format, JsArray, JsError, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes, __}
 
 object DelegationSerializer {
@@ -40,7 +40,10 @@ object DelegationSerializer {
     mapWrites[DelegateCreationId, DelegateCreationResponse](_.serialize, delegateCreationResponseWrites)
   private implicit val delegateSetMapSetErrorForCreationWrites: Writes[Map[DelegateCreationId, SetError]] =
     mapWrites[DelegateCreationId, SetError](_.serialize, setErrorWrites)
+  private implicit val delegationMapSetErrorForDeletionWrites: Writes[Map[UnparsedDelegateId, SetError]] =
+    mapWrites[UnparsedDelegateId, SetError](_.id.value, setErrorWrites)
   private implicit val delegateSetResponseWrites: OWrites[DelegateSetResponse] = Json.writes[DelegateSetResponse]
+  private implicit val delegatedAccountSetResponseWrites: OWrites[DelegatedAccountSetResponse] = Json.writes[DelegatedAccountSetResponse]
 
   private implicit val mapCreationRequestByDelegateCreationId: Reads[Map[DelegateCreationId, JsObject]] =
     Reads.mapReads[DelegateCreationId, JsObject] {string => refineV[IdConstraint](string)
@@ -58,6 +61,7 @@ object DelegationSerializer {
   private implicit val delegateIdsReads: Reads[DelegateIds] = Json.valueReads[DelegateIds]
   private implicit val delegateGetRequestReads: Reads[DelegateGetRequest] = Json.reads[DelegateGetRequest]
   private implicit val delegatedAccountGetRequestReads: Reads[DelegatedAccountGetRequest] = Json.reads[DelegatedAccountGetRequest]
+  private implicit val delegatedAccountSetRequestReads: Reads[DelegatedAccountSetRequest] = Json.reads[DelegatedAccountSetRequest]
   private implicit val usernameWrites: Writes[Username] = username => JsString(username.asString)
   private implicit val delegateWrites: Writes[Delegate] = Json.writes[Delegate]
   private implicit val delegateNotFoundWrites: Writes[DelegateNotFound] =
@@ -66,11 +70,14 @@ object DelegationSerializer {
     notFound => JsArray(notFound.value.toList.map(id => JsString(id.id.value)))
   private implicit val delegateGetResponseWrites: Writes[DelegateGetResponse] = Json.writes[DelegateGetResponse]
   private implicit val delegatedAccountGetResponseWrites: Writes[DelegatedAccountGetResponse] = Json.writes[DelegatedAccountGetResponse]
+
   def serializeDelegateSetResponse(response: DelegateSetResponse): JsObject = Json.toJsObject(response)
+  def serializeDelegatedAccountSetResponse(response: DelegatedAccountSetResponse): JsObject = Json.toJsObject(response)
   def deserializeDelegateSetRequest(input: JsValue): JsResult[DelegateSetRequest] = Json.fromJson[DelegateSetRequest](input)
   def deserializeDelegateCreationRequest(input: JsValue): JsResult[DelegateCreationRequest] = Json.fromJson[DelegateCreationRequest](input)
   def deserializeDelegateGetRequest(input: JsValue): JsResult[DelegateGetRequest] = Json.fromJson[DelegateGetRequest](input)
   def deserializeDelegatedAccountGetRequest(input: JsValue): JsResult[DelegatedAccountGetRequest] = Json.fromJson[DelegatedAccountGetRequest](input)
+  def deserializeDelegatedAccountSetRequest(input: JsValue): JsResult[DelegatedAccountSetRequest] = Json.fromJson[DelegatedAccountSetRequest](input)
 
   def serialize(delegateGetResponse: DelegateGetResponse, properties: Properties): JsValue =
     Json.toJson(delegateGetResponse)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/DelegatedAccountDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/DelegatedAccountDeletePerformer.scala
new file mode 100644
index 0000000000..067ee44887
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/DelegatedAccountDeletePerformer.scala
@@ -0,0 +1,77 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *  http://www.apache.org/licenses/LICENSE-2.0                  *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.method
+
+import javax.inject.Inject
+import org.apache.james.core.Username
+import org.apache.james.jmap.core.SetError
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.delegation.{DelegatedAccountSetRequest, DelegationId, UnparsedDelegateId}
+import org.apache.james.jmap.method.DelegatedAccountDeletePerformer.{DelegatedAccountDeletionFailure, DelegatedAccountDeletionResult, DelegatedAccountDeletionResults, DelegatedAccountDeletionSuccess}
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.user.api.DelegationStore
+import org.apache.james.util.ReactorUtils
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+object DelegatedAccountDeletePerformer {
+  sealed trait DelegatedAccountDeletionResult
+
+  case class DelegatedAccountDeletionSuccess(delegationId: DelegationId) extends DelegatedAccountDeletionResult
+
+  case class DelegatedAccountDeletionFailure(unparsedDelegateId: UnparsedDelegateId, exception: Throwable) extends DelegatedAccountDeletionResult {
+    def asSetError: SetError = exception match {
+      case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"${unparsedDelegateId.id} is not a DelegationId: ${e.getMessage}"))
+      case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
+    }
+  }
+
+  case class DelegatedAccountDeletionResults(results: Seq[DelegatedAccountDeletionResult]) {
+    def destroyed: Seq[DelegationId] =
+      results.flatMap(result => result match {
+        case success: DelegatedAccountDeletionSuccess => Some(success)
+        case _ => None
+      }).map(_.delegationId)
+
+    def retrieveErrors: Map[UnparsedDelegateId, SetError] =
+      results.flatMap(result => result match {
+        case failure: DelegatedAccountDeletionFailure => Some(failure.unparsedDelegateId, failure.asSetError)
+        case _ => None
+      }).toMap
+  }
+}
+
+class DelegatedAccountDeletePerformer @Inject()(delegationStore: DelegationStore) {
+  def delete(request: DelegatedAccountSetRequest, mailboxSession: MailboxSession): SMono[DelegatedAccountDeletionResults] =
+    SFlux.fromIterable(request.destroy.getOrElse(Seq()))
+      .flatMap(unparsedId => delete(unparsedId, mailboxSession.getUser)
+        .onErrorRecover(e => DelegatedAccountDeletionFailure(unparsedId, e)),
+        maxConcurrency = ReactorUtils.DEFAULT_CONCURRENCY)
+      .collectSeq()
+      .map(DelegatedAccountDeletionResults)
+
+  private def delete(unparsedId: UnparsedDelegateId, baseUser: Username): SMono[DelegatedAccountDeletionResult] =
+    unparsedId.parse
+      .fold(e => SMono.error(e),
+        id => SFlux(delegationStore.delegatedUsers(baseUser))
+          .filter(delegatedUser => DelegationId.from(delegatedUser, baseUser).equals(id))
+          .next()
+          .flatMap(delegatedUser => SMono(delegationStore.removeDelegatedUser(baseUser, delegatedUser)))
+          .`then`(SMono.just[DelegatedAccountDeletionResult](DelegatedAccountDeletionSuccess(id))))
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/DelegatedAccountSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/DelegatedAccountSetMethod.scala
new file mode 100644
index 0000000000..3f9318fc24
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/DelegatedAccountSetMethod.scala
@@ -0,0 +1,68 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ * http://www.apache.org/licenses/LICENSE-2.0                   *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.method
+
+import eu.timepit.refined.auto._
+import javax.inject.Inject
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JAMES_DELEGATION, JMAP_CORE}
+import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.core.{Invocation, SessionTranslator, UuidState}
+import org.apache.james.jmap.delegation.{DelegatedAccountSetRequest, DelegatedAccountSetResponse, ForbiddenAccountManagementException}
+import org.apache.james.jmap.json.{DelegationSerializer, ResponseSerializer}
+import org.apache.james.jmap.routes.SessionSupplier
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.mailbox.MailboxSession.isPrimaryAccount
+import org.apache.james.metrics.api.MetricFactory
+import play.api.libs.json.{JsError, JsSuccess}
+import reactor.core.scala.publisher.SMono
+
+class DelegatedAccountSetMethod @Inject()(deletePerformer: DelegatedAccountDeletePerformer,
+                                          val metricFactory: MetricFactory,
+                                          val sessionSupplier: SessionSupplier,
+                                          val sessionTranslator: SessionTranslator) extends MethodRequiringAccountId[DelegatedAccountSetRequest] {
+  override val methodName: Invocation.MethodName = MethodName("DelegatedAccount/set")
+  override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JAMES_DELEGATION)
+
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, DelegatedAccountSetRequest] =
+    DelegationSerializer.deserializeDelegatedAccountSetRequest(invocation.arguments.value) match {
+      case JsSuccess(delegatedAccountSetRequest, _) => Right(delegatedAccountSetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
+
+  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: DelegatedAccountSetRequest): SMono[InvocationWithContext] =
+    if (isPrimaryAccount(mailboxSession)) {
+      for {
+        destroyResults <- deletePerformer.delete(request, mailboxSession)
+      } yield InvocationWithContext(
+        invocation = Invocation(
+          methodName = methodName,
+          arguments = Arguments(DelegationSerializer.serializeDelegatedAccountSetResponse(DelegatedAccountSetResponse(
+            accountId = request.accountId,
+            oldState = None,
+            newState = UuidState.INSTANCE,
+            destroyed = Some(destroyResults.destroyed).filter(_.nonEmpty),
+            notDestroyed = Some(destroyResults.retrieveErrors).filter(_.nonEmpty)))),
+          methodCallId = invocation.invocation.methodCallId),
+        processingContext = invocation.processingContext)
+    } else {
+      SMono.error(ForbiddenAccountManagementException())
+    }
+}
+


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