Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ISSUE-1163] Implement OpenPaasRestClient #1228

Merged
merged 17 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions tmail-backend/tmail-third-party/openpaas/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.linagora.tmail</groupId>
<artifactId>tmail-third-party</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

<artifactId>tmail-openpaas</artifactId>
<name>Twake Mail :: Third Party :: OpenPaaS</name>
<description>OpenPaaS integration for Twake Mail</description>

<dependencies>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-core</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>james-server-testing</artifactId>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>testing-base</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
HoussemNasri marked this conversation as resolved.
Show resolved Hide resolved
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<scope>test</scope>
</dependency>
</dependencies>


<build>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
HoussemNasri marked this conversation as resolved.
Show resolved Hide resolved
</plugin>
<plugin>
<groupId>io.github.evis</groupId>
<artifactId>scalafix-maven-plugin_2.13</artifactId>
<configuration>
<config>${project.parent.parent.parent.basedir}/.scalafix.conf</config>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.linagora.tmail;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class HttpUtils {

/**
* Returns the AUTHORIZATION header value in order to implement basic authentication.
*<p>
* For more information see <a href="https://datatracker.ietf.org/doc/html/">RFC2617#section-2</a>.
* */
public static String createBasicAuthenticationToken(String user, String password) {
String userPassword = user + ":" + password;
byte[] base64UserPassword = Base64
.getEncoder()
.encode(userPassword.getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(base64UserPassword, StandardCharsets.UTF_8);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.linagora.tmail;

import java.net.URL;

public record OpenPaasConfiguration(URL restClientUrl, String restClientUser, String restClientPassword) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.linagora.tmail.api;

import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

import jakarta.mail.internet.AddressException;

import org.apache.james.core.MailAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.linagora.tmail.HttpUtils;
import com.linagora.tmail.OpenPaasConfiguration;

import reactor.core.publisher.Mono;
import reactor.netty.ByteBufMono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.HttpClientResponse;

public class OpenPaasRestClient {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenPaasRestClient.class);

private static final Duration RESPONSE_TIMEOUT = Duration.ofSeconds(10);
private static final String AUTHORIZATION_HEADER = "Authorization";
private final HttpClient client;
private final ObjectMapper deserializer = new ObjectMapper();

public OpenPaasRestClient(OpenPaasConfiguration openPaasConfiguration) {
URL apiUrl = openPaasConfiguration.restClientUrl();
String user = openPaasConfiguration.restClientUser();
String password = openPaasConfiguration.restClientPassword();
this.client = HttpClient.create()
.baseUrl(apiUrl.toString())
.headers(headers -> headers.add(AUTHORIZATION_HEADER, HttpUtils.createBasicAuthenticationToken(user, password)))
.responseTimeout(RESPONSE_TIMEOUT);
}

public Mono<MailAddress> retrieveMailAddress(String openPaasUserId) {
return client.get()
.uri(String.format("/users/%s", openPaasUserId))
.responseSingle((statusCode, data) -> handleUserResponse(openPaasUserId, statusCode, data))
.map(OpenPaasUserResponse::preferredEmail)
.map(this::parseMailAddress)
.onErrorResume(e -> Mono.error(new OpenPaasRestClientException("Failed to retrieve user mail using OpenPaas id", e)));
}

private Mono<OpenPaasUserResponse> handleUserResponse(String openPaasUserId, HttpClientResponse httpClientResponse, ByteBufMono dataBuf) {
int statusCode = httpClientResponse.status().code();

return switch (statusCode) {
case 200 -> dataBuf.asByteArray()
.map(this::parseUserResponse)
.onErrorResume(e -> Mono.error(new OpenPaasRestClientException("Bad user response body format", e)));
case 404 -> {
LOGGER.warn("Unable to retrieve mail address as OpenPaas user with id {} not found", openPaasUserId);
yield Mono.empty();
}
default -> dataBuf.asString(StandardCharsets.UTF_8)
.switchIfEmpty(Mono.just(""))
.flatMap(errorResponse -> Mono.error(new OpenPaasRestClientException(
String.format("""
Error when getting OpenPaas user response.\s
Response Status = %s,
Response Body = %s""", statusCode, errorResponse))));
};
}

private OpenPaasUserResponse parseUserResponse(byte[] responseAsBytes) {
try {
return deserializer.readValue(new String(responseAsBytes, StandardCharsets.UTF_8), OpenPaasUserResponse.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

private MailAddress parseMailAddress(String email) {
try {
return new MailAddress(email);
} catch (AddressException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.linagora.tmail.api;

public class OpenPaasRestClientException extends RuntimeException {

public OpenPaasRestClientException(String message) {
super(message);
}

public OpenPaasRestClientException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.linagora.tmail.api;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public record OpenPaasUserResponse(@JsonProperty("id") String id,
@JsonProperty("firstname") String firstname,
@JsonProperty("lastname") String lastname,
@JsonProperty("preferredEmail") String preferredEmail,
@JsonProperty("emails") List<String> emails,
@JsonProperty("main_phone") String mainPhone,
@JsonProperty("displayName") String displayName) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.linagora.tmail.api;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;

import jakarta.mail.internet.AddressException;

import org.apache.james.core.MailAddress;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.linagora.tmail.OpenPaasConfiguration;

public class OpenPaasRestClientTest {
public static final String BAD_USER_ID = "BAD_ID";
@RegisterExtension
static OpenPaasServerExtension openPaasServerExtension = new OpenPaasServerExtension();

OpenPaasRestClient restClient;

@BeforeEach
void setup() {
OpenPaasConfiguration openPaasConfig = new OpenPaasConfiguration(
openPaasServerExtension.getBaseUrl(),
OpenPaasServerExtension.GOOD_USER(),
OpenPaasServerExtension.GOOD_PASSWORD());

restClient = new OpenPaasRestClient(openPaasConfig);
}

@Test
void shouldReturnUserMailAddressWhenUserIdAndAuthenticationTokenIsCorrect()
throws AddressException {
assertThat(restClient.retrieveMailAddress(OpenPaasServerExtension.ALICE_USER_ID()).block())
.isEqualTo(new MailAddress(OpenPaasServerExtension.ALICE_EMAIL()));
}

@Test
void shouldReturnEmptyMonoWhenUserWithIdNotFound() {
assertThat(restClient.retrieveMailAddress(BAD_USER_ID).blockOptional()).isEmpty();
}

@Test
void shouldThrowExceptionOnErrorStatusCode() {
OpenPaasConfiguration openPaasConfig = new OpenPaasConfiguration(
openPaasServerExtension.getBaseUrl(),
OpenPaasServerExtension.BAD_USER(),
OpenPaasServerExtension.BAD_PASSWORD());

restClient = new OpenPaasRestClient(openPaasConfig);

assertThatThrownBy(() -> restClient.retrieveMailAddress(OpenPaasServerExtension.ALICE_USER_ID()).block())
.isInstanceOf(OpenPaasRestClientException.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.linagora.tmail.api

import java.net.{URI, URL}

import com.linagora.tmail.HttpUtils
import com.linagora.tmail.api.OpenPaasServerExtension.{ALICE_EMAIL, ALICE_USER_ID, BAD_AUTHENTICATION_TOKEN, GOOD_AUTHENTICATION_TOKEN, LOGGER}
import org.junit.jupiter.api.extension._
import org.mockserver.configuration.ConfigurationProperties
import org.mockserver.integration.ClientAndServer
import org.mockserver.integration.ClientAndServer.startClientAndServer
import org.mockserver.model.HttpRequest.request
import org.mockserver.model.HttpResponse.response
import org.mockserver.model.NottableString.string
import org.slf4j.{Logger, LoggerFactory}


object OpenPaasServerExtension {
val ALICE_USER_ID: String = "abc0a663bdaffe0026290xyz"
val ALICE_EMAIL: String = "[email protected]"
val GOOD_USER = "admin"
val GOOD_PASSWORD = "admin"
val BAD_USER = "BAD_USER"
val BAD_PASSWORD = "BAD_PASSWORD"
val GOOD_AUTHENTICATION_TOKEN: String = HttpUtils.createBasicAuthenticationToken(GOOD_USER, GOOD_PASSWORD)
val BAD_AUTHENTICATION_TOKEN: String = HttpUtils.createBasicAuthenticationToken(BAD_USER, BAD_PASSWORD)
private val LOGGER: Logger = LoggerFactory.getLogger(OpenPaasServerExtension.getClass)
}

class OpenPaasServerExtension extends BeforeEachCallback with AfterEachCallback with ParameterResolver{
var mockServer: ClientAndServer = _

override def beforeEach(context: ExtensionContext): Unit = {
mockServer = startClientAndServer(0)
ConfigurationProperties.logLevel("DEBUG")

mockServer.when(
request.withPath(s"/users/$ALICE_USER_ID")
.withMethod("GET")
.withHeader(string("Authorization"), string(BAD_AUTHENTICATION_TOKEN)))
.respond(response.withStatusCode(401))

mockServer.when(
request.withPath(s"/users/$ALICE_USER_ID")
.withMethod("GET")
.withHeader(string("Authorization"), string(GOOD_AUTHENTICATION_TOKEN)))
.respond(response.withStatusCode(200)
.withBody(s"""{
| "_id": "$ALICE_USER_ID",
| "firstname": "Alice",
| "lastname": "DOE",
| "preferredEmail": "$ALICE_EMAIL",
| "emails": [
| "$ALICE_EMAIL"
| ],
| "domains": [
| {
| "joined_at": "2020-09-03T08:16:35.682Z",
| "domain_id": "$ALICE_USER_ID"
| }
| ],
| "states": [],
| "avatars": [
| "$ALICE_USER_ID"
| ],
| "main_phone": "01111111111",
| "accounts": [
| {
| "timestamps": {
| "creation": "2020-09-03T08:16:35.682Z"
| },
| "hosted": true,
| "emails": [
| "[email protected]"
| ],
| "preferredEmailIndex": 0,
| "type": "email"
| }
| ],
| "login": {
| "failures": [],
| "success": "2024-10-04T12:59:44.469Z"
| },
| "id": "$ALICE_USER_ID",
| "displayName": "Alice DOE",
| "objectType": "user",
| "followers": 0,
| "followings": 0
|}""".stripMargin))
}

override def afterEach(context: ExtensionContext): Unit = {
if (mockServer == null) {
LOGGER.warn("Mock server is null")
} else {
mockServer.close()
}
}

override def supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean =
parameterContext.getParameter.getType eq classOf[ClientAndServer]

override def resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): AnyRef =
mockServer

def getBaseUrl: URL = new URI(s"http://localhost:${mockServer.getLocalPort}").toURL

}
1 change: 1 addition & 0 deletions tmail-backend/tmail-third-party/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@

<modules>
<module>open-ai</module>
<module>openpaas</module>
</modules>
</project>