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