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/05/29 03:03:15 UTC
[james-project] 08/08: JAMES-3093 Add BasicAuthenticationStrategy
for JMAP RFC-8621
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 81880375289776284c9fbc9f524dc1f5d3b22dab
Author: duc91 <du...@gmail.com>
AuthorDate: Tue May 26 14:54:14 2020 +0700
JAMES-3093 Add BasicAuthenticationStrategy for JMAP RFC-8621
---
.../DistributedBasicAuthenticationTest.java | 51 +++++++
.../rfc8621/contract/AuthenticationContract.scala | 151 +++++++++++++++++++++
.../jmap/rfc8621/contract/EchoMethodContract.scala | 83 +++--------
.../james/jmap/rfc8621/contract/Fixture.scala | 94 +++++++++++++
.../rfc8621/memory/MemoryAuthenticationTest.java | 38 ++++++
server/protocols/jmap-rfc-8621/pom.xml | 60 +++++---
.../jmap/http/BasicAuthenticationStrategy.scala | 109 +++++++++++++++
.../org/apache/james/jmap/http/SessionRoutes.scala | 7 +-
.../apache/james/jmap/routes/JMAPApiRoutes.scala | 34 +++--
.../james/jmap/http/UserCredentialParserTest.scala | 135 ++++++++++++++++++
.../james/jmap/routes/JMAPApiRoutesTest.scala | 130 +++++++++++++-----
11 files changed, 760 insertions(+), 132 deletions(-)
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/DistributedBasicAuthenticationTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBasicAuthenticationTest.java
new file mode 100644
index 0000000..1d7932b
--- /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/DistributedBasicAuthenticationTest.java
@@ -0,0 +1,51 @@
+/****************************************************************
+ * 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.DockerElasticSearchExtension;
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.AuthenticationContract;
+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 DistributedBasicAuthenticationTest implements AuthenticationContract {
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+ CassandraRabbitMQJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .blobStore(BlobStoreConfiguration.objectStorage().disableCache())
+ .build())
+ .extension(new DockerElasticSearchExtension())
+ .extension(new CassandraExtension())
+ .extension(new RabbitMQExtension())
+ .extension(new AwsS3BlobStoreExtension())
+ .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+}
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/AuthenticationContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/AuthenticationContract.scala
new file mode 100644
index 0000000..6716c1a
--- /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/AuthenticationContract.scala
@@ -0,0 +1,151 @@
+/****************************************************************
+ * 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 java.nio.charset.StandardCharsets.UTF_8
+import java.util.Base64
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured._
+import io.restassured.authentication.NoAuthScheme
+import io.restassured.http.{Header, Headers}
+import org.apache.http.HttpStatus.{SC_OK, SC_UNAUTHORIZED}
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.rfc8621.contract.AuthenticationContract._
+import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Tag, Test}
+
+object AuthenticationContract {
+ private val AUTHORIZATION_HEADER: String = "Authorization"
+}
+
+trait AuthenticationContract {
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent()
+ .addDomain(DOMAIN.asString())
+ .addUser(BOB.asString(), BOB_PASSWORD)
+ .addDomain(_2_DOT_DOMAIN.asString())
+ .addUser(ALICE.asString(), ALICE_PASSWORD)
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(new NoAuthScheme())
+ .build
+ }
+
+ @Test
+ def postShouldRespondUnauthorizedWhenNoAuthorizationHeader(): Unit = {
+ `given`()
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_UNAUTHORIZED)
+ }
+
+ @Test
+ @Tag(CategoryTags.BASIC_FEATURE)
+ def postShouldRespond200WhenHasCredentials(): Unit = {
+ val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.asString}:$BOB_PASSWORD")}")
+ `given`()
+ .headers(getHeadersWith(authHeader))
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_OK)
+ }
+
+ @Test
+ def postShouldRespond401WhenCredentialsWithInvalidUser(): Unit = {
+ val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.getLocalPart}@@$DOMAIN:$BOB_PASSWORD")}")
+ `given`()
+ .headers(getHeadersWith(authHeader))
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_UNAUTHORIZED)
+ }
+
+ @Test
+ def postShouldRespondOKWhenCredentialsWith2DotDomain(): Unit = {
+ val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${ALICE.asString}:$ALICE_PASSWORD")}")
+ `given`()
+ .headers(getHeadersWith(authHeader))
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_OK)
+ }
+
+ @Test
+ def postShouldRespond401WhenCredentialsWithSpaceDomain(): Unit = {
+ val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.getLocalPart}@$DOMAIN_WITH_SPACE:$BOB_PASSWORD")}")
+ `given`()
+ .headers(getHeadersWith(authHeader))
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_UNAUTHORIZED)
+ }
+
+ @Test
+ def postShouldRespond401WhenUserNotFound(): Unit = {
+ val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"usernotfound@$DOMAIN:$BOB_PASSWORD")}")
+ `given`()
+ .headers(getHeadersWith(authHeader))
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_UNAUTHORIZED)
+ }
+
+ @Test
+ @Tag(CategoryTags.BASIC_FEATURE)
+ def postShouldRespond401WhenWrongPassword(): Unit = {
+ val authHeader: Header = new Header(AUTHORIZATION_HEADER, s"Basic ${toBase64(s"${BOB.asString}:WRONG_PASSWORD")}")
+ `given`()
+ .headers(getHeadersWith(authHeader))
+ .body(ECHO_REQUEST_OBJECT)
+ .when()
+ .post()
+ .then
+ .statusCode(SC_UNAUTHORIZED)
+ }
+
+ private def getHeadersWith(authHeader: Header): Headers = {
+ new Headers(
+ new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+ authHeader
+ )
+ }
+
+ private def toBase64(stringValue: String): String = {
+ Base64.getEncoder.encodeToString(stringValue.getBytes(UTF_8))
+ }
+}
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/EchoMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala
index 79578fc..509106d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EchoMethodContract.scala
@@ -18,41 +18,20 @@
****************************************************************/
package org.apache.james.jmap.rfc8621.contract
-import java.nio.charset.StandardCharsets
-
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
-import io.restassured.RestAssured
-import io.restassured.builder.RequestSpecBuilder
-import io.restassured.config.EncoderConfig.encoderConfig
-import io.restassured.config.RestAssuredConfig.newConfig
-import io.restassured.http.ContentType
+import io.restassured.RestAssured._
+import io.restassured.http.ContentType.JSON
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
-import org.apache.http.HttpStatus
+import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
-import org.apache.james.jmap.JMAPUrls.JMAP
-import org.apache.james.jmap.draft.JmapGuiceProbe
+import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.EchoMethodContract._
+import org.apache.james.jmap.rfc8621.contract.Fixture._
import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
+import org.apache.james.utils.DataProbeImpl
import org.junit.jupiter.api.{BeforeEach, Tag, Test}
object EchoMethodContract {
-
- private val REQUEST_OBJECT: String =
- """{
- | "using": [
- | "urn:ietf:params:jmap:core"
- | ],
- | "methodCalls": [
- | [
- | "Core/echo",
- | {
- | "arg1": "arg1data",
- | "arg2": "arg2data"
- | },
- | "c1"
- | ]
- | ]
- |}""".stripMargin
private val REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD: String =
"""{
| "using": [
@@ -77,20 +56,6 @@ object EchoMethodContract {
| ]
|}""".stripMargin
- private val RESPONSE_OBJECT: String =
- """{
- | "sessionState": "75128aab4b1b",
- | "methodResponses": [
- | [
- | "Core/echo",
- | {
- | "arg1": "arg1data",
- | "arg2": "arg2data"
- | },
- | "c1"
- | ]
- | ]
- |}""".stripMargin
private val RESPONSE_OBJECT_WITH_UNSUPPORTED_METHOD: String =
"""{
| "sessionState": "75128aab4b1b",
@@ -112,55 +77,51 @@ object EchoMethodContract {
| ]
| ]
|}""".stripMargin
-
- private val ACCEPT_RFC8621_VERSION_HEADER: String = """application/json; jmapVersion=rfc-8621"""
}
trait EchoMethodContract {
@BeforeEach
def setUp(server: GuiceJamesServer): Unit = {
- RestAssured.requestSpecification = new RequestSpecBuilder()
- .setContentType(ContentType.JSON)
- .setAccept(ContentType.JSON)
- .setConfig(newConfig.encoderConfig(encoderConfig.defaultContentCharset(StandardCharsets.UTF_8)))
- .setPort(server.getProbe(classOf[JmapGuiceProbe])
- .getJmapPort
- .getValue)
- .setBasePath(JMAP)
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent()
+ .addDomain(DOMAIN.asString())
+ .addUser(BOB.asString(), BOB_PASSWORD)
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
.build
}
@Test
@Tag(CategoryTags.BASIC_FEATURE)
def echoMethodShouldRespondOKWithRFC8621VersionAndSupportedMethod(): Unit = {
- val response: String = RestAssured
- .`given`()
+
+ val response: String = `given`()
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(REQUEST_OBJECT)
+ .body(ECHO_REQUEST_OBJECT)
.when()
.post()
.then
- .statusCode(HttpStatus.SC_OK)
- .contentType(ContentType.JSON)
+ .statusCode(SC_OK)
+ .contentType(JSON)
.extract()
.body()
.asString()
- assertThatJson(response).isEqualTo(RESPONSE_OBJECT)
+ assertThatJson(response).isEqualTo(ECHO_RESPONSE_OBJECT)
}
@Test
def echoMethodShouldRespondWithRFC8621VersionAndUnsupportedMethod(): Unit = {
- val response: String = RestAssured
- .`given`()
+ val response: String = `given`()
.header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
.body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD)
.when()
.post()
.then
- .statusCode(HttpStatus.SC_OK)
- .contentType(ContentType.JSON)
+ .statusCode(SC_OK)
+ .contentType(JSON)
.extract()
.body()
.asString()
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
new file mode 100644
index 0000000..56be582
--- /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/Fixture.scala
@@ -0,0 +1,94 @@
+/****************************************************************
+ * 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 java.nio.charset.StandardCharsets
+
+import io.restassured.authentication.PreemptiveBasicAuthScheme
+import io.restassured.builder.RequestSpecBuilder
+import io.restassured.config.EncoderConfig.encoderConfig
+import io.restassured.config.RestAssuredConfig.newConfig
+import io.restassured.http.ContentType
+import org.apache.james.GuiceJamesServer
+import org.apache.james.core.{Domain, Username}
+import org.apache.james.jmap.JMAPUrls.JMAP
+import org.apache.james.jmap.draft.JmapGuiceProbe
+import org.apache.james.jmap.http.UserCredential
+
+object Fixture {
+ def baseRequestSpecBuilder(server: GuiceJamesServer) = new RequestSpecBuilder()
+ .setContentType(ContentType.JSON)
+ .setAccept(ContentType.JSON)
+ .setConfig(newConfig.encoderConfig(encoderConfig.defaultContentCharset(StandardCharsets.UTF_8)))
+ .setPort(server.getProbe(classOf[JmapGuiceProbe])
+ .getJmapPort
+ .getValue)
+ .setBasePath(JMAP)
+
+ def authScheme(userCredential: UserCredential): PreemptiveBasicAuthScheme = {
+ val authScheme: PreemptiveBasicAuthScheme = new PreemptiveBasicAuthScheme
+ authScheme.setUserName(userCredential.username.asString())
+ authScheme.setPassword(userCredential.password)
+
+ authScheme
+ }
+
+ val DOMAIN: Domain = Domain.of("domain.tld")
+ val DOMAIN_WITH_SPACE: Domain = Domain.of("dom ain.tld")
+ val _2_DOT_DOMAIN: Domain = Domain.of("do.main.tld")
+ val BOB: Username = Username.fromLocalPartWithDomain("bob", DOMAIN)
+ val ALICE: Username = Username.fromLocalPartWithDomain("alice", _2_DOT_DOMAIN)
+ val BOB_PASSWORD: String = "bobpassword"
+ val ALICE_PASSWORD: String = "alicepassword"
+
+ val ECHO_REQUEST_OBJECT: String =
+ """{
+ | "using": [
+ | "urn:ietf:params:jmap:core"
+ | ],
+ | "methodCalls": [
+ | [
+ | "Core/echo",
+ | {
+ | "arg1": "arg1data",
+ | "arg2": "arg2data"
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin
+
+ val ECHO_RESPONSE_OBJECT: String =
+ """{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Core/echo",
+ | {
+ | "arg1": "arg1data",
+ | "arg2": "arg2data"
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin
+
+ val ACCEPT_RFC8621_VERSION_HEADER: String = "application/json; jmapVersion=rfc-8621"
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java
new file mode 100644
index 0000000..c99bf0f
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryAuthenticationTest.java
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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.memory;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.AuthenticationContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryAuthenticationTest implements AuthenticationContract {
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+ .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+}
diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index ba30cd2..feccb0c 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -34,26 +34,25 @@
<dependencies>
<dependency>
- <groupId>com.typesafe.play</groupId>
- <artifactId>play-json_${scala.base}</artifactId>
- </dependency>
- <dependency>
- <groupId>eu.timepit</groupId>
- <artifactId>refined_${scala.base}</artifactId>
- <version>0.9.13</version>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
</dependency>
<dependency>
- <groupId>io.rest-assured</groupId>
- <artifactId>rest-assured</artifactId>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-api</artifactId>
+ <type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>io.projectreactor.netty</groupId>
- <artifactId>reactor-netty</artifactId>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-memory</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
- <groupId>io.projectreactor</groupId>
- <artifactId>reactor-scala-extensions_${scala.base}</artifactId>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>apache-james-mailbox-memory</artifactId>
+ <scope>test</scope>
+ <type>test-jar</type>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
@@ -61,26 +60,47 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
- <artifactId>james-server-jmap</artifactId>
+ <artifactId>james-server-data-api</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
- <artifactId>testing-base</artifactId>
+ <artifactId>james-server-data-memory</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
- <artifactId>apache-james-mailbox-api</artifactId>
+ <artifactId>james-server-jmap</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
- <artifactId>apache-james-mailbox-api</artifactId>
- <type>test-jar</type>
+ <artifactId>metrics-tests</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>net.javacrumbs.json-unit</groupId>
- <artifactId>json-unit-assertj</artifactId>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>testing-base</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.play</groupId>
+ <artifactId>play-json_${scala.base}</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>eu.timepit</groupId>
+ <artifactId>refined_${scala.base}</artifactId>
+ <version>0.9.13</version>
+ </dependency>
+ <dependency>
+ <groupId>io.projectreactor</groupId>
+ <artifactId>reactor-scala-extensions_${scala.base}</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.projectreactor.netty</groupId>
+ <artifactId>reactor-netty</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.rest-assured</groupId>
+ <artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala
new file mode 100644
index 0000000..52d9127
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/BasicAuthenticationStrategy.scala
@@ -0,0 +1,109 @@
+/** **************************************************************
+ * 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.http
+
+import java.util.Base64
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.auto._
+import eu.timepit.refined.refineV
+import eu.timepit.refined.string.MatchesRegex
+import javax.inject.Inject
+import org.apache.james.core.Username
+import org.apache.james.jmap.http.UserCredential._
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
+import org.apache.james.user.api.UsersRepository
+import org.slf4j.LoggerFactory
+import reactor.core.publisher.Mono
+import reactor.core.scala.publisher.SFlux
+import reactor.netty.http.server.HttpServerRequest
+
+import scala.compat.java8.StreamConverters._
+import scala.util.{Failure, Success, Try}
+
+object UserCredential {
+ type BasicAuthenticationHeaderValue = String Refined MatchesRegex["Basic [\\d\\w=]++"]
+ type CredentialsAsString = String Refined MatchesRegex[".*:.*"]
+
+ private val logger = LoggerFactory.getLogger(classOf[UserCredential])
+ private val BASIC_AUTHENTICATION_PREFIX: String = "Basic "
+
+ def parseUserCredentials(token: String): Option[UserCredential] = {
+ val refinedValue: Either[String, BasicAuthenticationHeaderValue] = refineV(token)
+
+ refinedValue match {
+ // Ignore Authentication headers not being Basic Auth
+ case Left(_) => None
+ case Right(value) => extractUserCredentialsAsString(value)
+ }
+ }
+
+ private def extractUserCredentialsAsString(token: BasicAuthenticationHeaderValue): Option[UserCredential] = {
+ val encodedCredentials = token.replace(BASIC_AUTHENTICATION_PREFIX, "")
+ val decodedCredentialsString = new String(Base64.getDecoder.decode(encodedCredentials))
+ val refinedValue: Either[String, CredentialsAsString] = refineV(decodedCredentialsString)
+
+ refinedValue match {
+ case Left(errorMessage: String) =>
+ logger.info(s"Supplied basic authentication credentials do not match expected format. $errorMessage")
+ None
+ case Right(value) => toCredential(value)
+ }
+ }
+
+ private def toCredential(token: CredentialsAsString): Option[UserCredential] = {
+ val partSeparatorIndex: Int = token.indexOf(':')
+ val usernameString: String = token.substring(0, partSeparatorIndex)
+ val passwordString: String = token.substring(partSeparatorIndex + 1)
+
+ Try(UserCredential(Username.of(usernameString), passwordString)) match {
+ case Success(credential) => Some(credential)
+ case Failure(throwable:IllegalArgumentException) =>
+ logger.info("Username is not valid", throwable)
+ None
+ case Failure(unexpectedException) =>
+ logger.error("Unexpected Exception", unexpectedException)
+ None
+ }
+ }
+}
+
+case class UserCredential(username: Username, password: String)
+
+class BasicAuthenticationStrategy @Inject()(val usersRepository: UsersRepository,
+ val mailboxManager: MailboxManager) extends AuthenticationStrategy {
+
+ override def createMailboxSession(httpRequest: HttpServerRequest): Mono[MailboxSession] = {
+ SFlux.fromStream(() => authHeaders(httpRequest).toScala[Stream])
+ .map(parseUserCredentials)
+ .handle(publishNext)
+ .filter(isValid)
+ .map(_.username)
+ .map(mailboxManager.createSystemSession)
+ .singleOrEmpty()
+ .asJava()
+ }
+
+ private def publishNext[T]: (Option[T], reactor.core.publisher.SynchronousSink[T]) => Unit =
+ (maybeT, sink) => maybeT.foreach(t => sink.next(t))
+
+ private def isValid(userCredential: UserCredential): Boolean =
+ usersRepository.test(userCredential.username, userCredential.password)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
index 465c212..a7efeab 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
@@ -24,12 +24,13 @@ import java.util.stream.Stream
import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpResponseStatus.OK
-import javax.inject.Inject
+import javax.inject.{Inject, Named}
import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8
import org.apache.james.jmap.JMAPRoutes.CORS_CONTROL
import org.apache.james.jmap.JMAPUrls.AUTHENTICATION
import org.apache.james.jmap.exceptions.UnauthorizedException
import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, LOGGER}
+import org.apache.james.jmap.http.rfc8621.InjectionKeys
import org.apache.james.jmap.json.Serializer
import org.apache.james.jmap.model.Session
import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
@@ -46,8 +47,8 @@ object SessionRoutes {
}
@Inject
-class SessionRoutes(val serializer: Serializer,
- val authenticator: Authenticator,
+class SessionRoutes(@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
+ val serializer: Serializer,
val sessionSupplier: SessionSupplier = new SessionSupplier()) extends JMAPRoutes {
private val generateSession: JMAPRoute.Action =
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 8ea7002..410ad77 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -27,22 +27,31 @@ import eu.timepit.refined.auto._
import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpResponseStatus.OK
-import javax.inject.Inject
+import javax.inject.{Inject, Named}
import org.apache.http.HttpStatus.SC_BAD_REQUEST
import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
import org.apache.james.jmap.JMAPUrls.JMAP
+import org.apache.james.jmap.exceptions.UnauthorizedException
+import org.apache.james.jmap.http.Authenticator
+import org.apache.james.jmap.http.rfc8621.InjectionKeys
import org.apache.james.jmap.json.Serializer
import org.apache.james.jmap.method.CoreEcho
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model.{Invocation, RequestObject, ResponseObject}
import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
+import org.slf4j.{Logger, LoggerFactory}
import play.api.libs.json.{JsError, JsSuccess, Json}
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.{SFlux, SMono}
import reactor.core.scheduler.Schedulers
import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
-class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes {
+object JMAPApiRoutes {
+ val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApiRoutes])
+}
+
+class JMAPApiRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
+ val serializer: Serializer) extends JMAPRoutes {
private val coreEcho = new CoreEcho
override def routes(): stream.Stream[JMAPRoute] = Stream.of(
@@ -56,8 +65,9 @@ class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes {
.corsHeaders())
private def post(httpServerRequest: HttpServerRequest, httpServerResponse: HttpServerResponse): Mono[Void] =
- this.requestAsJsonStream(httpServerRequest)
- .flatMap(requestObject => this.process(requestObject, httpServerResponse))
+ SMono(authenticator.authenticate(httpServerRequest))
+ .flatMap(_ => this.requestAsJsonStream(httpServerRequest)
+ .flatMap(requestObject => this.process(requestObject, httpServerResponse)))
.onErrorResume(throwable => handleError(throwable, httpServerResponse))
.subscribeOn(Schedulers.elastic)
.asJava()
@@ -101,14 +111,12 @@ class JMAPApiRoutes @Inject() (serializer: Serializer) extends JMAPRoutes {
invocation.methodCallId))
}
- private def handleError(throwable: Throwable, httpServerResponse: HttpServerResponse): SMono[Void] = {
- if (throwable.isInstanceOf[IllegalArgumentException]) {
- return SMono.fromPublisher(httpServerResponse.status(SC_BAD_REQUEST)
- .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
- .sendString(SMono.fromCallable(() => throwable.getMessage), StandardCharsets.UTF_8)
- .`then`())
- }
-
- SMono.fromPublisher(handleInternalError(httpServerResponse, throwable))
+ private def handleError(throwable: Throwable, httpServerResponse: HttpServerResponse): SMono[Void] = throwable match {
+ case exception: IllegalArgumentException => SMono.fromPublisher(httpServerResponse.status(SC_BAD_REQUEST)
+ .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
+ .sendString(SMono.fromCallable(() => exception.getMessage), StandardCharsets.UTF_8)
+ .`then`())
+ case exception: UnauthorizedException => SMono(handleAuthenticationFailure(httpServerResponse, JMAPApiRoutes.LOGGER, exception))
+ case _ => SMono.fromPublisher(handleInternalError(httpServerResponse, throwable))
}
}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala
new file mode 100644
index 0000000..8ac66c8
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/UserCredentialParserTest.scala
@@ -0,0 +1,135 @@
+/** **************************************************************
+ * 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.http
+
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+
+import org.apache.james.core.Username
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class UserCredentialParserTest {
+ @Test
+ def shouldReturnCredentialWhenUsernamePasswordToken(): Unit = {
+ val token: String = "Basic " + toBase64("user1:password")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("user1"), "password")))
+ }
+
+ @Test
+ def shouldAcceptPartSeparatorAsPartOfPassword(): Unit = {
+ val token: String = "Basic " + toBase64("user1:pass:word")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("user1"), "pass:word")))
+ }
+
+ @Test
+ def shouldReturnCredentialWhenRandomSpecialCharacterInUsernameToken(): Unit = {
+ val token: String = "Basic " + toBase64("fd2*#jk:password")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("fd2*#jk"), "password")))
+ }
+
+ @Test
+ def shouldReturnCredentialsWhenRandomSpecialCharacterInBothUsernamePasswoedToken(): Unit = {
+ val token: String = "Basic " + toBase64("fd2*#jk:password@fd23*&^$%")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("fd2*#jk"), "password@fd23*&^$%")))
+ }
+
+ @Test
+ def shouldReturnCredentialWhenUsernameDomainPasswordToken(): Unit = {
+ val token: String = "Basic " + toBase64("user1@domain.tld:password")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("user1@domain.tld"), "password")))
+ }
+
+ @Test
+ def shouldReturnCredentialWhenUsernameDomainNoPasswordToken(): Unit = {
+ val token: String = "Basic " + toBase64("user1@domain.tld:")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("user1@domain.tld"), "")))
+ }
+
+ @Test
+ def shouldReturnNoneWhenPayloadIsNotBase64(): Unit = {
+ val token: String = "Basic user1:password"
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(None)
+ }
+
+ @Test
+ def shouldReturnNoneWhenEmptyToken(): Unit = {
+ assertThat(UserCredential.parseUserCredentials(""))
+ .isEqualTo(None)
+ }
+
+ @Test
+ def shouldReturnNoneWhenWrongFormatCredential(): Unit = {
+ val token: String = "Basic " + toBase64("user1@password")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(None)
+ }
+
+ @Test
+ def shouldReturnNoneWhenUpperCaseToken(): Unit = {
+ val token: String = "BASIC " + toBase64("user1@password")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(None)
+ }
+
+ @Test
+ def shouldReturnNoneWhenLowerCaseToken(): Unit = {
+ val token: String = "basic " + toBase64("user1:password")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(None)
+ }
+
+ @Test
+ def shouldReturnNoneWhenCredentialWithNoPassword(): Unit = {
+ val token: String = "Basic " + toBase64("user1:")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(Some(UserCredential(Username.of("user1"), "")))
+ }
+
+ @Test
+ def shouldReturnEmptyWhenCredentialWithNoUsername(): Unit = {
+ val token: String = "Basic " + toBase64(":pass")
+
+ assertThat(UserCredential.parseUserCredentials(token))
+ .isEqualTo(None)
+ }
+
+ private def toBase64(stringValue: String): String = {
+ Base64.getEncoder.encodeToString(stringValue.getBytes(StandardCharsets.UTF_8))
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index 9bf7ba8..f23e835 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -19,6 +19,7 @@
package org.apache.james.jmap.routes
import java.nio.charset.StandardCharsets
+import java.util.Base64
import com.google.common.collect.ImmutableSet
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
@@ -26,28 +27,51 @@ import io.restassured.RestAssured
import io.restassured.builder.RequestSpecBuilder
import io.restassured.config.EncoderConfig.encoderConfig
import io.restassured.config.RestAssuredConfig.newConfig
-import io.restassured.http.ContentType
+import io.restassured.http.{ContentType, Header, Headers}
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
import org.apache.http.HttpStatus
+import org.apache.james.core.{Domain, Username}
+import org.apache.james.dnsservice.api.DNSService
+import org.apache.james.domainlist.memory.MemoryDomainList
import org.apache.james.jmap.JMAPUrls.JMAP
-import org.apache.james.jmap.json.Serializer
import org.apache.james.jmap._
+import org.apache.james.jmap.http.{Authenticator, BasicAuthenticationStrategy}
+import org.apache.james.jmap.json.Serializer
+import org.apache.james.jmap.routes.JMAPApiRoutesTest._
+import org.apache.james.mailbox.MailboxManager
+import org.apache.james.mailbox.extension.PreDeletionHook
+import org.apache.james.mailbox.inmemory.MemoryMailboxManagerProvider
import org.apache.james.mailbox.model.TestId
+import org.apache.james.metrics.tests.RecordingMetricFactory
+import org.apache.james.user.memory.MemoryUsersRepository
+import org.mockito.Mockito.mock
import org.scalatest.BeforeAndAfter
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
-class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
+object JMAPApiRoutesTest {
private val SERIALIZER: Serializer = new Serializer(new TestId.Factory)
-
private val TEST_CONFIGURATION: JMAPConfiguration = JMAPConfiguration.builder().enable().randomPort().build()
private val ACCEPT_JMAP_VERSION_HEADER = "application/json; jmapVersion="
private val ACCEPT_DRAFT_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.DRAFT.asString()
private val ACCEPT_RFC8621_VERSION_HEADER = ACCEPT_JMAP_VERSION_HEADER + Version.RFC8621.asString()
- private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(SERIALIZER)
+ private val empty_set: ImmutableSet[PreDeletionHook] = ImmutableSet.of()
+ private val dnsService = mock(classOf[DNSService])
+ private val domainList = new MemoryDomainList(dnsService)
+ domainList.addDomain(Domain.of("james.org"))
+
+ private val usersRepository = MemoryUsersRepository.withoutVirtualHosting(domainList)
+ usersRepository.addUser(Username.of("user1"), "password")
+
+ private val mailboxManager: MailboxManager = MemoryMailboxManagerProvider.provideMailboxManager(empty_set)
+ private val authenticationStrategy: BasicAuthenticationStrategy = new BasicAuthenticationStrategy(usersRepository, mailboxManager)
+ private val AUTHENTICATOR: Authenticator = Authenticator.of(new RecordingMetricFactory, authenticationStrategy)
+ private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, SERIALIZER)
private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE))
+ private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8))
+
private val REQUEST_OBJECT: String =
"""{
| "using": [
@@ -136,6 +160,9 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
| }
|}
|""".stripMargin
+}
+
+class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
var jmapServer: JMAPServer = _
@@ -158,88 +185,121 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
}
"RFC-8621 version, GET" should "not supported and return 404 status" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
+
RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .headers(headers)
.when()
- .get
+ .get
.then
- .statusCode(HttpStatus.SC_NOT_FOUND)
+ .statusCode(HttpStatus.SC_NOT_FOUND)
}
"RFC-8621 version, POST, without body" should "return 200 status" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
+
RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .headers(headers)
.when()
- .post
+ .post
.then
- .statusCode(HttpStatus.SC_OK)
+ .statusCode(HttpStatus.SC_OK)
}
"RFC-8621 version, POST, methods include supported" should "return OK status" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
+
val response = RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(REQUEST_OBJECT)
+ .headers(headers)
+ .body(REQUEST_OBJECT)
.when()
- .post()
+ .post()
.then
- .statusCode(HttpStatus.SC_OK)
- .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.SC_OK)
+ .contentType(ContentType.JSON)
.extract()
- .body()
- .asString()
+ .body()
+ .asString()
assertThatJson(response).isEqualTo(RESPONSE_OBJECT)
}
"RFC-8621 version, POST, with methods" should "return OK status, ResponseObject depend on method" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
+
val response = RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD)
+ .headers(headers)
+ .body(REQUEST_OBJECT_WITH_UNSUPPORTED_METHOD)
.when()
- .post()
+ .post()
.then
- .statusCode(HttpStatus.SC_OK)
- .contentType(ContentType.JSON)
+ .statusCode(HttpStatus.SC_OK)
+ .contentType(ContentType.JSON)
.extract()
- .body()
- .asString()
+ .body()
+ .asString()
assertThatJson(response).isEqualTo(RESPONSE_OBJECT_WITH_UNSUPPORTED_METHOD)
}
"Draft version, GET" should "return 404 status" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
+
RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER)
+ .headers(headers)
.when()
- .get
+ .get
.then
- .statusCode(HttpStatus.SC_NOT_FOUND)
+ .statusCode(HttpStatus.SC_NOT_FOUND)
}
"Draft version, POST, without body" should "return 400 status" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_DRAFT_VERSION_HEADER)
+ .headers(headers)
.when()
- .post
+ .post
.then
- .statusCode(HttpStatus.SC_NOT_FOUND)
+ .statusCode(HttpStatus.SC_NOT_FOUND)
}
"RFC-8621 version, POST, with wrong requestObject body" should "return 400 status" in {
+ val headers: Headers = Headers.headers(
+ new Header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER),
+ new Header("Authorization", s"Basic ${userBase64String}")
+ )
RestAssured
.`given`()
- .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
- .body(WRONG_OBJECT_REQUEST)
+ .headers(headers)
+ .body(WRONG_OBJECT_REQUEST)
.when()
- .post
+ .post
.then
- .statusCode(HttpStatus.SC_BAD_REQUEST)
+ .statusCode(HttpStatus.SC_BAD_REQUEST)
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org