nu.yona.server.subscriptions.rest.UserController.java Source code

Java tutorial

Introduction

Here is the source code for nu.yona.server.subscriptions.rest.UserController.java

Source

/*******************************************************************************
 * Copyright (c) 2015, 2018 Stichting Yona Foundation This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *******************************************************************************/
package nu.yona.server.subscriptions.rest;

import static nu.yona.server.rest.Constants.PASSWORD_HEADER;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;

import javax.annotation.PostConstruct;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.hateoas.ExposesResourceFor;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;

import nu.yona.server.DOSProtectionService;
import nu.yona.server.analysis.rest.UserActivityController;
import nu.yona.server.crypto.seckey.CryptoSession;
import nu.yona.server.crypto.seckey.SecretKeyUtil;
import nu.yona.server.device.rest.DeviceController;
import nu.yona.server.device.service.DeviceBaseDto;
import nu.yona.server.device.service.DeviceRegistrationRequestDto;
import nu.yona.server.device.service.DeviceService;
import nu.yona.server.device.service.UserDeviceDto;
import nu.yona.server.exceptions.ConfirmationException;
import nu.yona.server.exceptions.InvalidDataException;
import nu.yona.server.exceptions.YonaException;
import nu.yona.server.goals.rest.GoalController;
import nu.yona.server.goals.service.GoalDto;
import nu.yona.server.messaging.rest.MessageController;
import nu.yona.server.properties.YonaProperties;
import nu.yona.server.rest.Constants;
import nu.yona.server.rest.ControllerBase;
import nu.yona.server.rest.ErrorResponseDto;
import nu.yona.server.rest.GlobalExceptionMapping;
import nu.yona.server.rest.JsonRootRelProvider;
import nu.yona.server.rest.StandardResourcesController;
import nu.yona.server.subscriptions.rest.UserController.UserResource;
import nu.yona.server.subscriptions.service.BuddyDto;
import nu.yona.server.subscriptions.service.BuddyService;
import nu.yona.server.subscriptions.service.ConfirmationFailedResponseDto;
import nu.yona.server.subscriptions.service.UserDto;
import nu.yona.server.subscriptions.service.UserService;
import nu.yona.server.subscriptions.service.VPNProfileDto;
import nu.yona.server.util.Require;

@Controller
@ExposesResourceFor(UserResource.class)
@RequestMapping(value = "/users", produces = { MediaType.APPLICATION_JSON_VALUE })
public class UserController extends ControllerBase {
    private static final String REQUESTING_USER_ID_PARAM = "requestingUserId";
    public static final String REQUESTING_DEVICE_ID_PARAM = "requestingDeviceId";
    private static final String INCLUDE_PRIVATE_DATA_PARAM = "includePrivateData";
    private static final String TEMP_PASSWORD_PARAM = "tempPassword";
    private static final Map<String, Object> OMITTED_PARAMS;
    static {
        Map<String, Object> v = new HashMap<>();
        v.put(TEMP_PASSWORD_PARAM, null);
        v.put(INCLUDE_PRIVATE_DATA_PARAM, null);
        OMITTED_PARAMS = Collections.unmodifiableMap(v);
    }

    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Autowired
    private BuddyService buddyService;

    @Autowired
    private DeviceService deviceService;

    @Autowired
    private DOSProtectionService dosProtectionService;

    @Autowired
    private YonaProperties yonaProperties;

    @Autowired
    private CurieProvider curieProvider;

    @Autowired
    private GlobalExceptionMapping globalExceptionMapping;

    @Autowired
    private PinResetRequestController pinResetRequestController;

    @Autowired
    @Qualifier("sslRootCertificate")
    private X509Certificate sslRootCertificate; // YD-544

    @RequestMapping(value = "/{userId}", method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<UserResource> getUser(@RequestHeader(value = PASSWORD_HEADER) Optional<String> password,
            @RequestParam(value = TEMP_PASSWORD_PARAM, required = false) String tempPasswordStr,
            @RequestParam(value = REQUESTING_USER_ID_PARAM, required = false) String requestingUserIdStr,
            @RequestParam(value = REQUESTING_DEVICE_ID_PARAM, required = false) String requestingDeviceIdStr,
            @RequestParam(value = INCLUDE_PRIVATE_DATA_PARAM, required = false, defaultValue = "false") String includePrivateDataStr,
            @PathVariable UUID userId) {
        Optional<String> tempPassword = Optional.ofNullable(tempPasswordStr);
        Optional<String> passwordToUse = getPasswordToUse(password, tempPassword);
        return Optional.ofNullable(determineRequestingUserId(userId, requestingUserIdStr, includePrivateDataStr))
                .map(UUID::fromString)
                .map(ruid -> getUser(ruid, requestingDeviceIdStr, userId, tempPassword.isPresent(), passwordToUse))
                .orElseGet(() -> getPublicUser(userId));
    }

    /**
     * This method contains a backward compatibility mechanism: In the past, users were using the "includePrivateData" query
     * parameter to indicate they were fetching their own user entity including the private data. Nowadays, this is indicated by
     * setting requestingUserId to the ID of the user being fetched.<br/>
     * <br/>
     * Given that the old URL with includePriateData is stored on the devices of the users, that query parameter needs to to be
     * supported forever.
     */
    private String determineRequestingUserId(UUID userId, String requestingUserIdStr,
            String includePrivateDataStr) {
        return Boolean.TRUE.toString().equals(includePrivateDataStr) ? userId.toString() : requestingUserIdStr;
    }

    private HttpEntity<UserResource> getUser(UUID requestingUserId, String requestingDeviceIdStr, UUID userId,
            boolean isCreatedOnBuddyRequest, Optional<String> password) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(requestingUserId))) {
            if (requestingUserId.equals(userId)) {
                return getOwnUser(requestingUserId, requestingDeviceIdStr, isCreatedOnBuddyRequest);
            }
            return getBuddyUser(userId, requestingUserId);
        }
    }

    private HttpEntity<UserResource> getOwnUser(UUID userId, String requestingDeviceIdStr,
            boolean isCreatedOnBuddyRequest) {
        UserDto user = userService.getPrivateUser(userId, isCreatedOnBuddyRequest);
        Optional<UUID> requestingDeviceId = determineRequestingDeviceId(user, requestingDeviceIdStr,
                isCreatedOnBuddyRequest);
        if (requestingDeviceId.isPresent()
                && user.getOwnPrivateData().getDevices().map(d -> d.size() > 1).orElse(false)) {
            // User has multiple devices
            deviceService.removeDuplicateDefaultDevices(user, requestingDeviceId.get());
            user = userService.getPrivateUser(userId, isCreatedOnBuddyRequest);
        }
        return createOkResponse(user, createResourceAssemblerForOwnUser(userId, requestingDeviceId));
    }

    private ResponseEntity<UserResource> getBuddyUser(UUID userId, UUID requestingUserId) {
        return createOkResponse(buddyService.getUserOfBuddy(requestingUserId, userId),
                createResourceAssemblerForBuddy(requestingUserId));
    }

    private Optional<UUID> determineRequestingDeviceId(UserDto user, String requestingDeviceIdStr,
            boolean isCreatedOnBuddyRequest) {
        if (isCreatedOnBuddyRequest) {
            return Optional.empty();
        }
        return requestingDeviceIdStr == null ? Optional.of(deviceService.getDefaultDeviceId(user))
                : Optional.of(UUID.fromString(requestingDeviceIdStr));
    }

    private HttpEntity<UserResource> getPublicUser(UUID userId) {
        return createOkResponse(userService.getPublicUser(userId), createResourceAssemblerForPublicUser());
    }

    @RequestMapping(value = "/", method = RequestMethod.POST)
    @ResponseBody
    @ResponseStatus(HttpStatus.CREATED)
    public HttpEntity<UserResource> addUser(
            @RequestParam(value = "overwriteUserConfirmationCode", required = false) String overwriteUserConfirmationCode,
            @RequestBody PostPutUserDto postPutUser, HttpServletRequest request) {
        UserDto user = convertToUser(postPutUser);
        // use DOS protection to prevent overwrite user confirmation code brute forcing,
        // and to prevent enumeration of all occupied mobile numbers
        return dosProtectionService.executeAttempt(getAddUserLinkBuilder().toUri(), request,
                yonaProperties.getSecurity().getMaxCreateUserAttemptsPerTimeWindow(),
                () -> addUser(Optional.ofNullable(overwriteUserConfirmationCode), user));
    }

    private UserDto convertToUser(PostPutUserDto postPutUser) {
        return UserDto.createInstance(postPutUser.firstName, postPutUser.lastName, postPutUser.mobileNumber,
                postPutUser.nickname, postPutUser.getDevice());
    }

    @RequestMapping(value = "/{userId}", method = RequestMethod.PUT)
    @ResponseBody
    public HttpEntity<UserResource> updateUser(
            @RequestHeader(value = Constants.PASSWORD_HEADER) Optional<String> password,
            @RequestParam(value = TEMP_PASSWORD_PARAM, required = false) String tempPasswordStr,
            @PathVariable UUID userId,
            @RequestParam(value = REQUESTING_DEVICE_ID_PARAM, required = false) String requestingDeviceIdStr,
            @RequestBody PostPutUserDto postPutUser, HttpServletRequest request) {
        UserDto user = convertToUser(postPutUser);
        Optional<String> tempPassword = Optional.ofNullable(tempPasswordStr);
        if (tempPassword.isPresent()) {
            return updateUserCreatedOnBuddyRequest(password, userId, user, tempPassword.get());
        } else {
            return updateUser(password, userId, UUID.fromString(requestingDeviceIdStr), user, request);
        }
    }

    private HttpEntity<UserResource> updateUser(Optional<String> password, UUID userId, UUID requestingDeviceId,
            UserDto user, HttpServletRequest request) {
        Require.that(user.getOwnPrivateData().getDevices().orElse(Collections.emptySet()).isEmpty(),
                () -> InvalidDataException.extraProperty("deviceName",
                        "Embedding devices in an update request is only allowed when updating a user created on buddy request"));
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            // use DOS protection to prevent enumeration of all occupied mobile numbers
            return dosProtectionService.executeAttempt(
                    getUpdateUserLinkBuilder(userId, Optional.of(requestingDeviceId)).toUri(), request,
                    yonaProperties.getSecurity().getMaxUpdateUserAttemptsPerTimeWindow(),
                    () -> updateUser(userId, requestingDeviceId, user));
        }
    }

    private HttpEntity<UserResource> updateUserCreatedOnBuddyRequest(Optional<String> password, UUID userId,
            UserDto user, String tempPassword) {
        Require.isNotPresent(password, InvalidDataException::appProvidedPasswordNotSupported);
        addDefaultDeviceIfNotProvided(user);
        try (CryptoSession cryptoSession = CryptoSession.start(SecretKeyUtil.generateRandomSecretKey())) {
            UserDto updatedUser = userService.updateUserCreatedOnBuddyRequest(userId, tempPassword, user);
            UUID defaultDeviceId = deviceService.getDefaultDeviceId(updatedUser);
            return createOkResponse(updatedUser,
                    createResourceAssemblerForOwnUser(userId, Optional.of(defaultDeviceId)));
        }
    }

    private void addDefaultDeviceIfNotProvided(UserDto user) {
        Set<DeviceBaseDto> devices = user.getOwnPrivateData().getDevices().orElse(Collections.emptySet());
        if (devices.isEmpty()) {
            devices.add(deviceService.createDefaultUserDeviceDto());
        }
    }

    private HttpEntity<UserResource> updateUser(UUID userId, UUID requestingDeviceId, UserDto user) {
        return createOkResponse(userService.updateUser(userId, user),
                createResourceAssemblerForOwnUser(userId, Optional.of(requestingDeviceId)));
    }

    @RequestMapping(value = "/{userId}", method = RequestMethod.DELETE)
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public void deleteUser(@RequestHeader(value = Constants.PASSWORD_HEADER) Optional<String> password,
            @PathVariable UUID userId, @RequestParam(value = "message", required = false) String messageStr) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            userService.deleteUser(userId, Optional.ofNullable(messageStr));
        }
    }

    @RequestMapping(value = "/{userId}/confirmMobileNumber", method = RequestMethod.POST)
    @ResponseBody
    public HttpEntity<UserResource> confirmMobileNumber(
            @RequestHeader(value = Constants.PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @RequestParam(value = REQUESTING_DEVICE_ID_PARAM, required = false) UUID requestingDeviceId,
            @RequestBody ConfirmationCodeDto mobileNumberConfirmation) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            return createOkResponse(userService.confirmMobileNumber(userId, mobileNumberConfirmation.getCode()),
                    createResourceAssemblerForOwnUser(userId, Optional.of(requestingDeviceId)));
        }
    }

    @RequestMapping(value = "/{userId}/resendMobileNumberConfirmationCode", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<Void> resendMobileNumberConfirmationCode(
            @RequestHeader(value = Constants.PASSWORD_HEADER) Optional<String> password,
            @PathVariable UUID userId) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            userService.resendMobileNumberConfirmationCode(userId);
            return createOkResponse();
        }
    }

    @ExceptionHandler(ConfirmationException.class)
    private ResponseEntity<ErrorResponseDto> handleException(ConfirmationException e, HttpServletRequest request) {
        if (e.getRemainingAttempts() >= 0) {
            ErrorResponseDto responseMessage = new ConfirmationFailedResponseDto(e.getMessageId(), e.getMessage(),
                    e.getRemainingAttempts());
            logger.error("Confirmation failed", e);
            return new ResponseEntity<>(responseMessage, e.getStatusCode());
        }
        return globalExceptionMapping.handleYonaException(e, request);
    }

    static ControllerLinkBuilder getAddUserLinkBuilder() {
        UserController methodOn = methodOn(UserController.class);
        return linkTo(methodOn.addUser(null, null, null));
    }

    static ControllerLinkBuilder getUpdateUserLinkBuilder(UUID userId, Optional<UUID> requestingDeviceId) {
        return linkTo(methodOn(UserController.class).updateUser(Optional.empty(), null, userId,
                requestingDeviceId.map(UUID::toString).orElse(null), null, null));
    }

    static ControllerLinkBuilder getConfirmMobileNumberLinkBuilder(UUID userId, UUID requestingDeviceId) {
        UserController methodOn = methodOn(UserController.class);
        return linkTo(methodOn.confirmMobileNumber(Optional.empty(), userId, requestingDeviceId, null));
    }

    private HttpEntity<UserResource> addUser(Optional<String> overwriteUserConfirmationCode, UserDto user) {
        addDefaultDeviceIfNotProvided(user);
        try (CryptoSession cryptoSession = CryptoSession.start(SecretKeyUtil.generateRandomSecretKey())) {
            UserDto createdUser = userService.addUser(user, overwriteUserConfirmationCode);
            UUID requestingDeviceId = deviceService.getDefaultDeviceId(createdUser);
            return createResponse(createdUser, HttpStatus.CREATED,
                    createResourceAssemblerForOwnUser(createdUser.getId(), Optional.of(requestingDeviceId)));
        }
    }

    private Optional<String> getPasswordToUse(Optional<String> password, Optional<String> tempPassword) {
        if (password.isPresent()) {
            return password;
        }
        if (tempPassword.isPresent()) {
            return tempPassword;
        }
        return Optional.empty();
    }

    public UserResourceAssembler createResourceAssemblerForOwnUser(UUID requestingUserId,
            Optional<UUID> requestingDeviceId) {
        return UserResourceAssembler.createInstanceForOwnUser(curieProvider, requestingUserId, requestingDeviceId,
                pinResetRequestController);
    }

    private UserResourceAssembler createResourceAssemblerForBuddy(UUID requestingUserId) {
        return UserResourceAssembler.createInstanceForBuddy(curieProvider, requestingUserId);
    }

    private UserResourceAssembler createResourceAssemblerForPublicUser() {
        return UserResourceAssembler.createPublicUserInstance(curieProvider);
    }

    static Link getUserSelfLinkWithTempPassword(UUID userId, String tempPassword) {
        ControllerLinkBuilder linkBuilder = linkTo(methodOn(UserController.class).getUser(Optional.empty(),
                tempPassword, userId.toString(), null, null, userId));
        // Should call expand, but that's not done because of https://github.com/spring-projects/spring-hateoas/issues/703
        return linkBuilder.withSelfRel();
    }

    private static Link getConfirmMobileLink(UUID userId, Optional<UUID> requestingDeviceId) {
        ControllerLinkBuilder linkBuilder = linkTo(methodOn(UserController.class)
                .confirmMobileNumber(Optional.empty(), userId, requestingDeviceId.orElse(null), null));
        return linkBuilder.withRel("confirmMobileNumber");
    }

    public static Link getResendMobileNumberConfirmationLink(UUID userId) {
        ControllerLinkBuilder linkBuilder = linkTo(
                methodOn(UserController.class).resendMobileNumberConfirmationCode(Optional.empty(), userId));
        return linkBuilder.withRel("resendMobileNumberConfirmationCode");
    }

    private static Link getUserLink(String rel, UUID userId, Optional<UUID> requestingUserId,
            Optional<UUID> requestingDeviceId) {
        String requestingUserIdStr = requestingUserId.map(UUID::toString).orElse(null);
        String requestingDeviceIdStr = requestingDeviceId.map(UUID::toString).orElse(null);
        return linkTo(methodOn(UserController.class).getUser(Optional.empty(), null, requestingUserIdStr,
                requestingDeviceIdStr, null, userId)).withRel(rel).expand(OMITTED_PARAMS);
    }

    public static Link getPublicUserLink(String rel, UUID userId) {
        return getUserLink(rel, userId, Optional.empty(), Optional.empty());
    }

    public static Link getPrivateUserLink(String rel, UUID userId, Optional<UUID> requestingDeviceId) {
        return getUserLink(rel, userId, Optional.of(userId), requestingDeviceId);
    }

    public static Link getBuddyUserLink(String rel, UUID userId, UUID requestingUserId) {
        return getUserLink(rel, userId, Optional.of(requestingUserId), Optional.empty());
    }

    @PostConstruct
    private void setSslRootCertificateCn() // YD-544
    {
        try {
            LdapName name = new LdapName(sslRootCertificate.getIssuerX500Principal().getName());
            UserResource.setSslRootCertificateCn(name.getRdn(0).getValue().toString());
        } catch (InvalidNameException e) {
            throw YonaException.unexpected(e);
        }
    }

    static class PostPutUserDto {
        private final String firstName;
        private final String lastName;
        private final String mobileNumber;
        private final String nickname;
        private final Optional<DeviceRegistrationRequestDto> deviceRegistration;

        @JsonCreator
        public PostPutUserDto(@JsonProperty("firstName") String firstName,
                @JsonProperty("lastName") String lastName, @JsonProperty("mobileNumber") String mobileNumber,
                @JsonProperty("nickname") String nickname,
                @JsonProperty(value = "deviceName", required = false) String deviceName,
                @JsonProperty(value = "deviceOperatingSystem", required = false) String deviceOperatingSystem,
                @JsonProperty(value = "deviceAppVersion", required = false) String deviceAppVersion,
                @JsonProperty(value = "deviceAppVersionCode", required = false) Integer deviceAppVersionCode,
                @JsonProperty("_links") Object ignored1, @JsonProperty("yonaPassword") Object ignored2) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.mobileNumber = mobileNumber;
            this.nickname = nickname;
            this.deviceRegistration = deviceName == null ? Optional.empty()
                    : Optional.of(new DeviceRegistrationRequestDto(deviceName, deviceOperatingSystem,
                            deviceAppVersion, deviceAppVersionCode, Optional.empty()));
        }

        Optional<UserDeviceDto> getDevice() {
            return deviceRegistration.map(UserDeviceDto::createDeviceRegistrationInstance);
        }
    }

    enum UserResourceRepresentation {
        MINIMAL(u -> false, u -> false, u -> false), BUDDY_USER(u -> true, u -> false, u -> false), OWN_USER(
                u -> u.isMobileNumberConfirmed(), u -> !u.isMobileNumberConfirmed(),
                u -> u.isMobileNumberConfirmed());

        final Function<UserDto, Boolean> includeGeneralContent;
        final Function<UserDto, Boolean> includeOwnUserNumNotConfirmedContent;
        final Function<UserDto, Boolean> includeOwnUserNumConfirmedContent;

        UserResourceRepresentation(Function<UserDto, Boolean> includeGeneralContent,
                Function<UserDto, Boolean> includeOwnUserNumNotConfirmedContent,
                Function<UserDto, Boolean> includeOwnUserNumConfirmedContent) {
            this.includeGeneralContent = includeGeneralContent;
            this.includeOwnUserNumNotConfirmedContent = includeOwnUserNumNotConfirmedContent;
            this.includeOwnUserNumConfirmedContent = includeOwnUserNumConfirmedContent;
        }
    }

    public static class UserResource extends Resource<UserDto> {
        private final CurieProvider curieProvider;
        private static String sslRootCertificateCn; // YD-544
        private UserResourceRepresentation representation;
        private Optional<UUID> requestingDeviceId;

        public UserResource(CurieProvider curieProvider, UserResourceRepresentation representation, UserDto user,
                Optional<UUID> requestingDeviceId) {
            super(user);
            this.curieProvider = curieProvider;
            this.representation = representation;
            this.requestingDeviceId = requestingDeviceId;
        }

        public static void setSslRootCertificateCn(String sslRootCertificateCn) {
            UserResource.sslRootCertificateCn = sslRootCertificateCn;
        }

        @JsonProperty("sslRootCertCN")
        @JsonInclude(Include.NON_EMPTY)
        public Optional<String> getSslRootCertCn() // YD-544
        {
            if (representation.includeOwnUserNumConfirmedContent.apply(getContent())) {
                return Optional.of(sslRootCertificateCn);
            }
            return Optional.empty();

        }

        @JsonProperty("_embedded")
        @JsonInclude(Include.NON_EMPTY)
        public Map<String, Object> getEmbeddedResources() {
            UUID userId = getContent().getId();
            HashMap<String, Object> result = new HashMap<>();
            if (representation.includeGeneralContent.apply(getContent())) {
                Optional<Set<DeviceBaseDto>> devices = getContent().getPrivateData().getDevices();
                devices.ifPresent(d -> result.put(curieProvider.getNamespacedRelFor(UserDto.DEVICES_REL_NAME),
                        DeviceController.createAllDevicesCollectionResource(userId, d, requestingDeviceId)));

                Optional<Set<GoalDto>> goals = getContent().getPrivateData().getGoals();
                goals.ifPresent(g -> result.put(curieProvider.getNamespacedRelFor(UserDto.GOALS_REL_NAME),
                        GoalController.createAllGoalsCollectionResource(userId, g)));
            }
            if (representation.includeOwnUserNumConfirmedContent.apply(getContent())) {
                Set<BuddyDto> buddies = getContent().getOwnPrivateData().getBuddies();
                result.put(curieProvider.getNamespacedRelFor(UserDto.BUDDIES_REL_NAME),
                        BuddyController.createAllBuddiesCollectionResource(curieProvider, userId, buddies));
            }

            return result;
        }

        // Remove this method as part of YD-541
        @JsonInclude(Include.NON_EMPTY)
        public Resource<VPNProfileDto> getVpnProfile() {
            if (representation.includeOwnUserNumConfirmedContent.apply(getContent())) {
                return getContent().getOwnPrivateData().getVpnProfile()
                        .map(DeviceController.DeviceResource::createVpnProfileResource).orElse(null);
            }
            return null;
        }

        static ControllerLinkBuilder getAllBuddiesLinkBuilder(UUID requestingUserId) {
            BuddyController methodOn = methodOn(BuddyController.class);
            return linkTo(methodOn.getAllBuddies(null, requestingUserId));
        }
    }

    public static class UserResourceAssembler extends ResourceAssemblerSupport<UserDto, UserResource> {
        private final CurieProvider curieProvider;
        private final Optional<UUID> requestingUserId;
        private final Optional<UUID> requestingDeviceId;
        private final Optional<PinResetRequestController> pinResetRequestController;
        private final UserResourceRepresentation representation;

        private UserResourceAssembler(UserResourceRepresentation representation, CurieProvider curieProvider,
                Optional<UUID> requestingUserId, Optional<UUID> requestingDeviceId,
                Optional<PinResetRequestController> pinResetRequestController) {
            super(UserController.class, UserResource.class);
            this.representation = representation;
            this.curieProvider = curieProvider;
            this.pinResetRequestController = pinResetRequestController;
            this.requestingUserId = requestingUserId;
            this.requestingDeviceId = requestingDeviceId;
        }

        public static UserResourceAssembler createPublicUserInstance(CurieProvider curieProvider) {
            return new UserResourceAssembler(UserResourceRepresentation.MINIMAL, curieProvider, Optional.empty(),
                    Optional.empty(), Optional.empty());
        }

        public static UserResourceAssembler createMinimalInstance(CurieProvider curieProvider,
                UUID requestingUserId) {
            return new UserResourceAssembler(UserResourceRepresentation.MINIMAL, curieProvider,
                    Optional.of(requestingUserId), Optional.empty(), Optional.empty());
        }

        public static UserResourceAssembler createInstanceForBuddy(CurieProvider curieProvider,
                UUID requestingUserId) {
            return new UserResourceAssembler(UserResourceRepresentation.BUDDY_USER, curieProvider,
                    Optional.of(requestingUserId), Optional.empty(), Optional.empty());
        }

        public static UserResourceAssembler createInstanceForOwnUser(CurieProvider curieProvider,
                UUID requestingUserId, Optional<UUID> requestingDeviceId,
                PinResetRequestController pinResetRequestController) {
            return new UserResourceAssembler(UserResourceRepresentation.OWN_USER, curieProvider,
                    Optional.of(requestingUserId), requestingDeviceId, Optional.of(pinResetRequestController));
        }

        @Override
        public UserResource toResource(UserDto user) {
            UserResource userResource = instantiateResource(user);
            addSelfLink(userResource);
            if (representation.includeGeneralContent.apply(user)) {
                addGeneralLinks(userResource);
            }
            if (representation.includeOwnUserNumNotConfirmedContent.apply(user)) {
                addOwnUserNumNotConfirmedLinks(user, userResource);
            }
            if (representation.includeOwnUserNumConfirmedContent.apply(user)) {
                addOwnUserNumConfirmedLinks(userResource);
            }
            return userResource;
        }

        private void addGeneralLinks(UserResource userResource) {
            addUserPhotoLinkIfPhotoPresent(userResource);
        }

        private void addOwnUserNumNotConfirmedLinks(UserDto user, UserResource userResource) {
            addEditLink(userResource);
            if (!user.isCreatedOnBuddyRequest()) {
                addConfirmMobileNumberLink(userResource);
                addResendMobileNumberConfirmationLink(userResource);
            }
        }

        private void addOwnUserNumConfirmedLinks(UserResource userResource) {
            addEditLink(userResource);
            addPostOpenAppEventLink(userResource); // YD-544
            addMessagesLink(userResource);
            addDayActivityOverviewsLink(userResource);
            addWeekActivityOverviewsLink(userResource);
            addDayActivityOverviewsWithBuddiesLink(userResource);
            addNewDeviceRequestLink(userResource);
            addAppActivityLink(userResource); // YD-544
            pinResetRequestController.get().addLinks(userResource);
            addSslRootCertificateLink(userResource); // YD-544
            addAppleMobileConfigLink(userResource); // YD-544
            addEditUserPhotoLink(userResource);
        }

        private void addEditUserPhotoLink(UserResource userResource) {
            userResource.add(linkTo(methodOn(UserPhotoController.class).uploadUserPhoto(Optional.empty(), null,
                    userResource.getContent().getId())).withRel("editUserPhoto").expand());
        }

        private void addUserPhotoLinkIfPhotoPresent(UserResource userResource) {
            userResource.getContent().getPrivateData().getUserPhotoId().ifPresent(userPhotoId -> userResource.add(
                    linkTo(methodOn(UserPhotoController.class).getUserPhoto(userPhotoId)).withRel("userPhoto")));
        }

        private void addAppleMobileConfigLink(UserResource userResource) {
            userResource.add(linkTo(methodOn(DeviceController.class).getAppleMobileConfig(Optional.empty(),
                    userResource.getContent().getId(), requestingDeviceId.get())).withRel("appleMobileConfig"));
        }

        private void addSslRootCertificateLink(Resource<UserDto> userResource) {
            userResource.add(
                    linkTo(methodOn(StandardResourcesController.class).getSslRootCert()).withRel("sslRootCert"));
        }

        @Override
        protected UserResource instantiateResource(UserDto user) {
            return new UserResource(curieProvider, representation, user, requestingDeviceId);
        }

        private void addSelfLink(Resource<UserDto> userResource) {
            if (userResource.getContent().getId() == null) {
                // removed user
                return;
            }

            userResource.add(UserController.getUserLink(Link.REL_SELF, userResource.getContent().getId(),
                    requestingUserId, requestingDeviceId));
        }

        private void addEditLink(Resource<UserDto> userResource) {
            userResource.add(getUpdateUserLinkBuilder(userResource.getContent().getId(), requestingDeviceId)
                    .withRel(JsonRootRelProvider.EDIT_REL).expand());
        }

        private void addConfirmMobileNumberLink(Resource<UserDto> userResource) {
            userResource.add(
                    UserController.getConfirmMobileLink(userResource.getContent().getId(), requestingDeviceId));
        }

        private static void addResendMobileNumberConfirmationLink(Resource<UserDto> userResource) {
            userResource
                    .add(UserController.getResendMobileNumberConfirmationLink(userResource.getContent().getId()));
        }

        private void addPostOpenAppEventLink(Resource<UserDto> userResource) {
            userResource.add(DeviceController.getPostOpenAppEventLink(userResource.getContent().getId(),
                    requestingDeviceId.get()));
        }

        private void addWeekActivityOverviewsLink(UserResource userResource) {
            userResource.add(UserActivityController
                    .getUserWeekActivityOverviewsLinkBuilder(userResource.getContent().getId())
                    .withRel(UserActivityController.WEEK_OVERVIEW_LINK));
        }

        private void addDayActivityOverviewsLink(UserResource userResource) {
            userResource.add(
                    UserActivityController.getUserDayActivityOverviewsLinkBuilder(userResource.getContent().getId())
                            .withRel(UserActivityController.DAY_OVERVIEW_LINK));
        }

        private void addDayActivityOverviewsWithBuddiesLink(UserResource userResource) {
            userResource.add(UserActivityController
                    .getDayActivityOverviewsWithBuddiesLinkBuilder(userResource.getContent().getId(),
                            requestingDeviceId.get())
                    .withRel("dailyActivityReportsWithBuddies"));
        }

        private void addMessagesLink(UserResource userResource) {
            userResource.add(MessageController.getMessagesLink(userResource.getContent().getId()));
        }

        private void addNewDeviceRequestLink(UserResource userResource) {
            userResource.add(NewDeviceRequestController
                    .getNewDeviceRequestLinkBuilder(userResource.getContent().getMobileNumber())
                    .withRel("newDeviceRequest"));
        }

        private void addAppActivityLink(UserResource userResource) {
            // IDs are available when the app activity link is relevant, so directly call get on Optional
            userResource.add(DeviceController.getAppActivityLink(requestingUserId.get(), requestingDeviceId.get()));
        }
    }
}