Skip to content

Commit

Permalink
feat: implement /users/myself API to expose the user locale prefs (#25)
Browse files Browse the repository at this point in the history
- Updated the openapi schema adding:
  - a new entrypoint "/users/myself" necessary to get the UserInfo
    with its Locale attribute;
  - a new enumerator (Locale) representing all the locales supported by
    Carbonio.
- Implemented the related methods in the UserApiController and in the
  UserService to retrieve the full details of the User (requester) via
  the SoapClient;
- Added the SoapClient#getAccountInfoByAuthToken method to retrieve the
  full details of the requester using its cookie;
- Added the ServiceException and the ServiceExceptionMapper to better
  handling the internal service errors: they avoid to have checked
  exceptions but it allows to handle the scenario where the service
  throws errors (expecially in the SoapClient);
- Added ITs for the GetMyselfByCookie API;
- Added UTs for the controller;
- Added logback.xml in prod and create the /var/log/user-management folder
  during the installation phase;
- Added dep5 file and removed .license files.

refs: UM-20
  • Loading branch information
federicorispo committed Aug 31, 2023
1 parent 9c61d5c commit a24b7c0
Show file tree
Hide file tree
Showing 25 changed files with 675 additions and 38 deletions.
41 changes: 41 additions & 0 deletions .reuse/dep5
@@ -0,0 +1,41 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: carbonio-user-management
Upstream-Contact: Zextras <packages@zextras.com>
Source: https://github.com/Zextras/carbonio-user-management

Files: pacur.json
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: package/intentions.json
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: package/policies.json
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: package/service-protocol.json
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: core/src/test/resources/soap/requests/GetAccountInfoRequest.xml
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: core/src/test/resources/soap/requests/GetInfoRequest.xml
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: core/src/test/resources/soap/responses/GetAccountInfoResponse.xml
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: core/src/test/resources/soap/responses/GetInfoResponse.xml
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

Files: core/src/test/resources/soap/responses/SoapNotFoundError.xml
Copyright: 2023 Zextras s.r.l
License: AGPL-3.0-only

40 changes: 40 additions & 0 deletions boot/src/main/resources/logback.xml
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
SPDX-License-Identifier: AGPL-3.0-only
-->

<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/carbonio/user-management/user-management.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>/var/log/carbonio/user-management/user-management.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- each file should be at most 10MB-->
<maxFileSize>10MB</maxFileSize>
<!-- keep 50 days' worth of history -->
<maxHistory>50</maxHistory>
<!-- Keep at maximum 500MB of logs -->
<totalSizeCap>500MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="ROLLING"/>
<appender-ref ref="STDOUT"/>
</root>

</configuration>
Expand Up @@ -9,6 +9,7 @@
import com.zextras.carbonio.user_management.controllers.AuthApiController;
import com.zextras.carbonio.user_management.controllers.HealthApiController;
import com.zextras.carbonio.user_management.controllers.UsersApiController;
import com.zextras.carbonio.user_management.exceptions.ServiceExceptionMapper;
import com.zextras.carbonio.user_management.generated.AuthApi;
import com.zextras.carbonio.user_management.generated.AuthApiService;
import com.zextras.carbonio.user_management.generated.HealthApi;
Expand All @@ -22,6 +23,7 @@ public class UserManagementModule extends RequestScopeModule {
@Override
protected void configure() {
bind(JacksonJsonProvider.class);
bind(ServiceExceptionMapper.class);

bind(HealthApi.class);
bind(HealthApiService.class).to(HealthApiController.class);
Expand Down
Expand Up @@ -62,4 +62,17 @@ public Response getUsersInfo(String cookie, List<String> userIds, SecurityContex
: Response.status(Status.BAD_REQUEST).build();
}

@Override
public Response getMyselfByCookie(String cookie, SecurityContext securityContext) {
Map<String, String> cookies = CookieParser.getCookies(cookie);

if (cookies.containsKey("ZM_AUTH_TOKEN")) {
return userService
.getMyselfByToken(cookies.get("ZM_AUTH_TOKEN"))
.map(userMyself -> Response.ok().entity(userMyself).build())
.orElse(Response.status(Status.NOT_FOUND).build());
}

return Response.status(Status.BAD_REQUEST).build();
}
}
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
//
// SPDX-License-Identifier: AGPL-3.0-only

package com.zextras.carbonio.user_management.exceptions;

/**
* Exception to be used when something bad happen during the execution of a request or when a
* dependency (like the carbonio-mailbox) responds with an unexpected/unhandled error.
*/
public class ServiceException extends RuntimeException {

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

public ServiceException(String message, Throwable throwable) {
super(message, throwable);
}
}
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
//
// SPDX-License-Identifier: AGPL-3.0-only

package com.zextras.carbonio.user_management.exceptions;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

/**
* Maps the {@link ServiceException} exception into a jax-rs {@link Response} with an
* {@link Status#INTERNAL_SERVER_ERROR} status code.
*/
@Provider
public class ServiceExceptionMapper implements ExceptionMapper<ServiceException> {

@Override
public Response toResponse(ServiceException exception) {
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage()).build();
}
}
Expand Up @@ -9,15 +9,21 @@
import com.sun.xml.ws.fault.ServerSOAPFaultException;
import com.zextras.carbonio.user_management.cache.CacheManager;
import com.zextras.carbonio.user_management.entities.UserToken;
import com.zextras.carbonio.user_management.exceptions.ServiceException;
import com.zextras.carbonio.user_management.generated.model.Locale;
import com.zextras.carbonio.user_management.generated.model.UserId;
import com.zextras.carbonio.user_management.generated.model.UserInfo;
import com.zextras.carbonio.user_management.generated.model.UserMyself;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import zimbraaccount.Attr;
import zimbraaccount.GetAccountInfoResponse;
import zimbraaccount.GetInfoResponse;
import zimbraaccount.Pref;

public class UserService {

Expand Down Expand Up @@ -136,6 +142,53 @@ public Response getInfoByEmail(
return Response.ok().entity(userInfo).build();
}

public Optional<UserMyself> getMyselfByToken(String token) {
try {
GetInfoResponse infoResponse = SoapClient
.newClient()
.setAuthToken(token)
.getAccountInfoByAuthToken();

UserId userId = new UserId();
userId.setUserId(infoResponse.getId());

String locale = infoResponse
.getPrefs()
.getPref()
.stream()
.filter(perf -> perf.getName().equals("zimbraPrefLocale"))
.findFirst()
.map(Pref::getValue)
.orElse("en");

String fullName = infoResponse
.getAttrs()
.getAttr()
.stream()
.filter(attribute -> attribute.getName().equals("displayName"))
.findFirst()
.map(Attr::getValue)
.orElse("");

UserMyself userMyself = new UserMyself();
userMyself.setId(userId);
userMyself.setEmail(infoResponse.getName());
userMyself.setDomain(infoResponse.getPublicURL());
userMyself.setFullName(fullName);
userMyself.setLocale(Locale.valueOf(locale.toUpperCase()));

return Optional.of(userMyself);

} catch (ServerSOAPFaultException exception) {
System.out.println(exception.getMessage());
return Optional.empty();
} catch (Exception exception) {
exception.printStackTrace();
throw new ServiceException(
"Unable to get account user info due to an internal service error");
}
}

public Response validateUserToken(String token) {
System.out.println("Validate: " + token);
// We can't use Optional.ofNullable because validateAuthToken throws exceptions and
Expand Down
Expand Up @@ -110,6 +110,10 @@ public Simulator start() {
return this;
}

public void resetAll() {
mailboxServiceMock.reset();
}

public void stopAll() {
stopJettyServer();
stopMailboxService();
Expand Down
Expand Up @@ -15,6 +15,15 @@
*/
public class SoapHttpUtils {

/**
* Creates the SoapNotFoundError XML body.
*
* @return a {@link String} representing the XML body response containing the NotFound error.
*/
public String getSoapNotFoundErrorResponse() {
return getXmlFile("soap/responses/SoapNotFoundError.xml");
}

/**
* Creates the GetAccountInfoRequest XML body substituting the auth token and the account id in
* the related placeholders.
Expand All @@ -24,9 +33,9 @@ public class SoapHttpUtils {
* @return a {@link String} representing the XML body request for the GetAccountInfo API.
*/
public String getAccountInfoRequest(String authToken, String accountId) {
String getInfoRequest = getXmlFile("soap/requests/GetAccountInfoRequest.xml");
String getAccountInfoRequest = getXmlFile("soap/requests/GetAccountInfoRequest.xml");

return getInfoRequest
return getAccountInfoRequest
.replaceAll("%AUTH_TOKEN%", authToken)
.replaceAll("%ACCOUNT_ID%", accountId);
}
Expand All @@ -44,15 +53,57 @@ public String getAccountInfoRequest(String authToken, String accountId) {
*/
public String getAccountInfoResponse(String accountId, String accountEmail, String accountDomain,
String accountFullName) {
String getInfoRequest = getXmlFile("soap/responses/GetAccountInfoResponse.xml");
String getAccountInfoResponse = getXmlFile("soap/responses/GetAccountInfoResponse.xml");

return getInfoRequest
return getAccountInfoResponse
.replaceAll("%ACCOUNT_ID%", accountId)
.replaceAll("%ACCOUNT_EMAIL%", accountEmail)
.replaceAll("%ACCOUNT_DOMAIN%", accountDomain)
.replaceAll("%ACCOUNT_FULL_NAME%", accountFullName);
}

/**
* Creates the GetInfoRequest XML body substituting the auth token in the related placeholders.
*
* @param authToken is a {@links String} representing the auth token of the requester
* @return a {@link String} representing the XML body request for the GetInfo API.
*/
public String getInfoRequest(String authToken) {
String getInfoRequest = getXmlFile("soap/requests/GetInfoRequest.xml");

return getInfoRequest.replaceAll("%AUTH_TOKEN%", authToken);
}

/**
* Creates the GetInfoResponse XML body substituting the input parameters in the related
* placeholders.
*
* @param accountId is a {@link String} representing the identifier of the retrieved
* account
* @param accountEmail is a {@link String} representing the email of the retrieved account
* @param accountDomain is a {@link String} representing the domain of the retrieved account
* @param accountFullName is a {@link String} representing the full name of the retrieved account
* @param accountLocale is a {@link String} representing the locale chosen by the retrieved
* account
* @return a {@link String} representing the XML payload response for the GetInfo API.
*/
public String getInfoResponse(
String accountId,
String accountEmail,
String accountDomain,
String accountFullName,
String accountLocale
) {
String getInfoResponse = getXmlFile("soap/responses/GetInfoResponse.xml");

return getInfoResponse
.replaceAll("%ACCOUNT_ID%", accountId)
.replaceAll("%ACCOUNT_EMAIL%", accountEmail)
.replaceAll("%ACCOUNT_DOMAIN%", accountDomain)
.replaceAll("%ACCOUNT_FULL_NAME%", accountFullName)
.replaceAll("%ACCOUNT_LOCALE%", accountLocale);
}

/**
* Allows to load the request/response xml file in memory to performs some substitution.
*
Expand Down

0 comments on commit a24b7c0

Please sign in to comment.