nu.yona.server.device.rest.DeviceController.java Source code

Java tutorial

Introduction

Here is the source code for nu.yona.server.device.rest.DeviceController.java

Source

/*******************************************************************************
 * Copyright (c) 2017, 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.device.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.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import javax.annotation.PostConstruct;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;

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.Resources;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
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.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 org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

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.analysis.service.AnalysisEngineProxyService;
import nu.yona.server.analysis.service.AppActivitiesDto;
import nu.yona.server.analysis.service.AppActivitiesDto.Activity;
import nu.yona.server.crypto.seckey.CryptoSession;
import nu.yona.server.device.entities.DeviceAnonymized.OperatingSystem;
import nu.yona.server.device.rest.DeviceController.DeviceResource;
import nu.yona.server.device.service.AppOpenEventDto;
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.DeviceServiceException;
import nu.yona.server.device.service.DeviceUpdateRequestDto;
import nu.yona.server.device.service.UserDeviceDto;
import nu.yona.server.exceptions.InvalidDataException;
import nu.yona.server.exceptions.YonaException;
import nu.yona.server.properties.YonaProperties;
import nu.yona.server.rest.Constants;
import nu.yona.server.rest.ControllerBase;
import nu.yona.server.rest.JsonRootRelProvider;
import nu.yona.server.rest.StandardResourcesController;
import nu.yona.server.subscriptions.rest.AppleMobileConfigSigner;
import nu.yona.server.subscriptions.rest.UserController;
import nu.yona.server.subscriptions.rest.UserController.UserResource;
import nu.yona.server.subscriptions.service.NewDeviceRequestDto;
import nu.yona.server.subscriptions.service.NewDeviceRequestService;
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;
import nu.yona.server.util.ThymeleafUtil;

@Controller
@ExposesResourceFor(DeviceResource.class)
@RequestMapping(value = "/users/{userId}/devices", produces = { MediaType.APPLICATION_JSON_VALUE })
public class DeviceController extends ControllerBase {
    private static final Logger logger = LoggerFactory.getLogger(DeviceController.class);

    @Autowired
    private DeviceService deviceService;

    @Autowired
    private UserService userService;

    @Autowired
    private UserController userController;

    @Autowired
    private AppleMobileConfigSigner appleMobileConfigSigner;

    @Autowired
    @Qualifier("appleMobileConfigTemplateEngine")
    private TemplateEngine templateEngine;

    @Autowired
    private NewDeviceRequestService newDeviceRequestService;

    @Autowired
    private YonaProperties yonaProperties;

    @Autowired
    private AnalysisEngineProxyService analysisEngineProxyService;

    @Autowired
    @Qualifier("sslRootCertificate")
    private X509Certificate sslRootCertificate;

    private enum MessageType {
        ERROR, WARNING
    }

    @RequestMapping(value = "/", method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<Resources<DeviceResource>> getAllDevices(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @RequestParam(value = UserController.REQUESTING_DEVICE_ID_PARAM, required = false) String requestingDeviceIdStr) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            Optional<UUID> requestingDeviceId = nullableStringToOptionalUuid(requestingDeviceIdStr);
            return createOkResponse(deviceService.getDevicesOfUser(userId),
                    createResourceAssembler(userId, requestingDeviceId),
                    getAllDevicesLinkBuilder(userId, requestingDeviceId));
        }
    }

    @RequestMapping(value = "/{deviceId}", method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<DeviceResource> getDevice(@RequestHeader(value = PASSWORD_HEADER) Optional<String> password,
            @PathVariable UUID userId, @PathVariable UUID deviceId,
            @RequestParam(value = UserController.REQUESTING_DEVICE_ID_PARAM, required = false) String requestingDeviceIdStr) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            return createOkResponse(deviceService.getDevice(deviceId),
                    createResourceAssembler(userId, nullableStringToOptionalUuid(requestingDeviceIdStr)));
        }
    }

    @RequestMapping(value = "/", method = RequestMethod.POST)
    @ResponseBody
    public HttpEntity<UserResource> registerDevice(
            @RequestHeader(value = Constants.NEW_DEVICE_REQUEST_PASSWORD_HEADER) String newDeviceRequestPassword,
            @PathVariable UUID userId, @RequestBody DeviceRegistrationRequestDto request) {
        NewDeviceRequestDto newDeviceRequest = newDeviceRequestService.getNewDeviceRequestForUser(userId,
                Optional.of(newDeviceRequestPassword));
        try (CryptoSession cryptoSession = CryptoSession.start(newDeviceRequest.getYonaPassword(),
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            UserDeviceDto newDevice = deviceService.addDeviceToUser(userId,
                    UserDeviceDto.createDeviceRegistrationInstance(request));
            return createResponse(userService.getPrivateUser(userId, false), HttpStatus.CREATED,
                    userController.createResourceAssemblerForOwnUser(userId, Optional.of(newDevice.getId())));
        }
    }

    @RequestMapping(value = "/{deviceId}/openApp", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<Void> postOpenAppEvent(
            @RequestHeader(value = Constants.PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable UUID deviceId, @RequestBody AppOpenEventDto request) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            assertValidOpenAppEvent(request);
            deviceService.postOpenAppEvent(userId, deviceId, request.getOperatingSystem(),
                    Optional.ofNullable(request.appVersion), request.appVersionCode);
            return createOkResponse();
        }
    }

    private void assertValidOpenAppEvent(AppOpenEventDto request) {
        if (request.operatingSystemStr == null) {
            Require.isNull(request.appVersion, () -> InvalidDataException.extraProperty("appVersion",
                    "If the operating system is not provided, the other properties should not be provided either"));
            Require.that(request.appVersionCode == 0, () -> InvalidDataException.extraProperty("appVersionCode",
                    "If the operating system is not provided, the other properties should not be provided either"));
        } else {
            Require.isNonNull(request.appVersion, () -> InvalidDataException.missingProperty("appVersion",
                    "If the operating system is provided, the other properties must be present too"));
            Require.that(request.appVersionCode != 0, () -> InvalidDataException.missingProperty("appVersionCode",
                    "If the operating system is provided, the other properties must be present too"));
        }
    }

    @RequestMapping(value = "/{deviceId}/apple.mobileconfig", method = RequestMethod.GET)
    @ResponseBody
    public ResponseEntity<byte[]> getAppleMobileConfig(
            @RequestHeader(value = Constants.PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable UUID deviceId) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(new MediaType("application", "x-apple-aspen-config"));
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            return new ResponseEntity<>(getDeviceSpecificAppleMobileConfig(deviceService.getDevice(deviceId)),
                    headers, HttpStatus.OK);
        }
    }

    private byte[] getDeviceSpecificAppleMobileConfig(UserDeviceDto device) {
        Context ctx = ThymeleafUtil.createContext();
        ctx.setVariable("ldapUsername", device.getVpnProfile().getVpnLoginId());
        ctx.setVariable("ldapPassword", device.getVpnProfile().getVpnPassword());

        return signIfEnabled(templateEngine.process("apple.mobileconfig", ctx).getBytes(StandardCharsets.UTF_8));
    }

    private byte[] signIfEnabled(byte[] unsignedMobileconfig) {
        if (yonaProperties.getAppleMobileConfig().isSigningEnabled()) {
            return appleMobileConfigSigner.sign(unsignedMobileconfig);
        }
        return unsignedMobileconfig;
    }

    @RequestMapping(value = "/{deviceId}", method = RequestMethod.PUT)
    @ResponseBody
    public HttpEntity<DeviceResource> updateDevice(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable UUID deviceId,
            @RequestParam(value = UserController.REQUESTING_DEVICE_ID_PARAM, required = false) String requestingDeviceIdStr,
            @RequestBody DeviceUpdateRequestDto request) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            return createOkResponse(deviceService.updateDevice(userId, deviceId, request),
                    createResourceAssembler(userId, nullableStringToOptionalUuid(requestingDeviceIdStr)));
        }
    }

    @RequestMapping(value = "/{deviceId}", method = RequestMethod.DELETE)
    @ResponseBody
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteDevice(@RequestHeader(value = PASSWORD_HEADER) Optional<String> password,
            @PathVariable UUID userId, @PathVariable UUID deviceId,
            @RequestParam(value = UserController.REQUESTING_DEVICE_ID_PARAM, required = false) String requestingDeviceIdStr) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            deviceService.deleteDevice(userId, deviceId);
        }
    }

    /*
     * Adds app activity registered by the Yona app. This request is delegated to the analysis engine service.
     * @param password User password, validated before adding the activity.
     * @param appActivities Because it may be that multiple app activities may have taken place during the time the network is
     * down, accept an array of activities.
     */
    @RequestMapping(value = "/{deviceId}/appActivity/", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<Void> addAppActivity(@RequestHeader(value = PASSWORD_HEADER) Optional<String> password,
            @PathVariable UUID userId, @PathVariable UUID deviceId, @RequestBody AppActivitiesDto appActivities) {
        if (appActivities.getActivities().length > yonaProperties.getAnalysisService()
                .getAppActivityCountIgnoreThreshold()) {
            logLongAppActivityBatch(MessageType.ERROR, userId, appActivities);
            return createOkResponse();
        }
        if (appActivities.getActivities().length > yonaProperties.getAnalysisService()
                .getAppActivityCountLoggingThreshold()) {
            logLongAppActivityBatch(MessageType.WARNING, userId, appActivities);
        }
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            UserDto userDto = userService.getPrivateUser(userId);
            autoregisterAndroid(userDto, deviceId);
            UUID userAnonymizedId = userDto.getOwnPrivateData().getUserAnonymizedId();
            UUID deviceAnonymizedId = deviceService.getDeviceAnonymizedId(userDto, deviceId);
            analysisEngineProxyService.analyzeAppActivity(userAnonymizedId, deviceAnonymizedId, appActivities);
            return createOkResponse();
        }
    }

    /**
     * Autoregisters the given device as running Android. As of today, only the Android app is capable of sending app activities,
     * so if the operating system of the requesting device is marked as UNKNOWN, we can improve the registration and mark it as
     * ANDROID.
     * 
     * @param userDto The user sending the app activities
     * @param deviceId The ID of the device for which the app activities are being sent
     */
    private void autoregisterAndroid(UserDto userDto, UUID deviceId) {
        UserDeviceDto device = userDto.getOwnPrivateData().getOwnDevices().stream()
                .filter(d -> d.getId().equals(deviceId)).findAny()
                .orElseThrow(() -> DeviceServiceException.notFoundById(deviceId));
        if (device.getOperatingSystem() == OperatingSystem.UNKNOWN) {
            // The device is registered as UNKNOWN, but given that it registers app activities, it's apparently ANDROID, so update
            // the operating system
            deviceService.updateOperatingSystem(userDto.getId(), deviceId, OperatingSystem.ANDROID);
        }
    }

    private void logLongAppActivityBatch(MessageType messageType, UUID userId, AppActivitiesDto appActivities) {
        int numAppActivities = appActivities.getActivities().length;
        List<Activity> appActivityCollection = Arrays.asList(appActivities.getActivities());
        Comparator<? super Activity> comparator = (a, b) -> a.getStartTime().compareTo(b.getStartTime());
        ZonedDateTime minStartTime = Collections.min(appActivityCollection, comparator).getStartTime();
        ZonedDateTime maxStartTime = Collections.max(appActivityCollection, comparator).getStartTime();
        switch (messageType) {
        case ERROR:
            logger.error(
                    "User with ID {} posts too many ({}) app activities, with start dates ranging from {} to {} (device time: {}). App activities ignored.",
                    userId, numAppActivities, minStartTime, maxStartTime, appActivities.getDeviceDateTime());
            break;
        case WARNING:
            logger.warn(
                    "User with ID {} posts many ({}) app activities, with start dates ranging from {} to {} (device time: {})",
                    userId, numAppActivities, minStartTime, maxStartTime, appActivities.getDeviceDateTime());
            break;
        default:
            throw new IllegalStateException("Unsupported message type: " + messageType);
        }
    }

    @RequestMapping(value = "/{deviceId}/vpnStatus/", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<Void> postVpnStatusChangeEvent(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable UUID deviceId, @RequestBody VpnStatusDto vpnStatus) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            deviceService.registerVpnStatusChangeEvent(userId, deviceId, vpnStatus.vpnConnected);
            return createNoContentResponse();
        }
    }

    public static Link getPostOpenAppEventLink(UUID userId, UUID deviceId) {
        ControllerLinkBuilder linkBuilder = linkTo(
                methodOn(DeviceController.class).postOpenAppEvent(Optional.empty(), userId, deviceId, null));
        return linkBuilder.withRel("postOpenAppEvent");
    }

    public static Link getAppActivityLink(UUID userId, UUID deviceId) {
        try {
            ControllerLinkBuilder linkBuilder = linkTo(
                    methodOn(DeviceController.class).addAppActivity(Optional.empty(), userId, deviceId, null));
            return linkBuilder.withRel("appActivity");
        } catch (SecurityException e) {
            throw YonaException.unexpected(e);
        }
    }

    public static Link getPostVpnStatusEventLink(UUID userId, UUID deviceId) {
        ControllerLinkBuilder linkBuilder = linkTo(methodOn(DeviceController.class)
                .postVpnStatusChangeEvent(Optional.empty(), userId, deviceId, null));
        return linkBuilder.withRel("postVpnStatusEvent");
    }

    private DeviceResourceAssembler createResourceAssembler(UUID userId, Optional<UUID> requestingDeviceId) {
        return new DeviceResourceAssembler(userId, requestingDeviceId);
    }

    public static ControllerLinkBuilder getAllDevicesLinkBuilder(UUID userId, Optional<UUID> requestingDeviceId) {
        DeviceController methodOn = methodOn(DeviceController.class);
        return linkTo(methodOn.getAllDevices(null, userId, optionalUuidToNullableString(requestingDeviceId)));
    }

    private static String optionalUuidToNullableString(Optional<UUID> optionalUuid) {
        return optionalUuid.map(UUID::toString).orElse(null);
    }

    private static Optional<UUID> nullableStringToOptionalUuid(String uuidStr) {
        return Optional.ofNullable(uuidStr).map(UUID::fromString);
    }

    public static ControllerLinkBuilder getDeviceLinkBuilder(UUID userId, UUID deviceId,
            Optional<UUID> requestingDeviceId) {
        DeviceController methodOn = methodOn(DeviceController.class);
        return linkTo(methodOn.getDevice(Optional.empty(), userId, deviceId,
                optionalUuidToNullableString(requestingDeviceId)));
    }

    public static ControllerLinkBuilder getRegisterDeviceLinkBuilder(UUID userId) {
        DeviceController methodOn = methodOn(DeviceController.class);
        return linkTo(methodOn.registerDevice(null, userId, null));
    }

    public static Resources<DeviceResource> createAllDevicesCollectionResource(UUID userId,
            Set<DeviceBaseDto> devices, Optional<UUID> requestingDeviceId) {
        return new Resources<>(new DeviceResourceAssembler(userId, requestingDeviceId).toResources(devices),
                DeviceController.getAllDevicesLinkBuilder(userId, requestingDeviceId).withSelfRel());
    }

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

    static class VpnStatusDto {
        private final boolean vpnConnected;

        @JsonCreator
        public VpnStatusDto(@JsonProperty("vpnConnected") boolean vpnConnected) {
            this.vpnConnected = vpnConnected;
        }
    }

    public static class DeviceResource extends Resource<DeviceBaseDto> {
        private static String sslRootCertificateCn;
        private final boolean isRequestingDevice;

        public DeviceResource(DeviceBaseDto device, boolean isRequestingDevice) {
            super(device);
            this.isRequestingDevice = isRequestingDevice;
        }

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

        public boolean isRequestingDevice() {
            return isRequestingDevice;
        }

        @JsonProperty("firebaseInstanceId")
        @JsonInclude(Include.NON_EMPTY)
        public Optional<String> getFirebaseInstanceId() {
            if (getContent() instanceof UserDeviceDto) {
                return ((UserDeviceDto) getContent()).getFirebaseInstanceId();
            }
            return Optional.empty();
        }

        @JsonProperty("sslRootCertCN")
        @JsonInclude(Include.NON_EMPTY)
        public Optional<String> getSslRootCertCn() {
            if (isRequestingDevice) {
                return Optional.of(sslRootCertificateCn);
            }
            return Optional.empty();
        }

        @JsonInclude(Include.NON_EMPTY)
        public Resource<VPNProfileDto> getVpnProfile() {
            if (getContent() instanceof UserDeviceDto) {
                return createVpnProfileResource(((UserDeviceDto) getContent()).getVpnProfile());
            }
            return null;
        }

        public static Resource<VPNProfileDto> createVpnProfileResource(VPNProfileDto vpnProfileDto) {
            Resource<VPNProfileDto> vpnProfileResource = new Resource<>(vpnProfileDto);
            addOvpnProfileLink(vpnProfileResource);
            return vpnProfileResource;
        }

        private static void addOvpnProfileLink(Resource<VPNProfileDto> vpnProfileResource) {
            vpnProfileResource.add(new Link(ServletUriComponentsBuilder.fromCurrentContextPath()
                    .path("/vpn/profile.ovpn").build().toUriString(), "ovpnProfile"));
        }
    }

    public static class DeviceResourceAssembler extends ResourceAssemblerSupport<DeviceBaseDto, DeviceResource> {
        private final UUID userId;
        private Optional<UUID> requestingDeviceId;

        public DeviceResourceAssembler(UUID userId, Optional<UUID> requestingDeviceId) {
            super(DeviceController.class, DeviceResource.class);
            this.userId = userId;
            this.requestingDeviceId = requestingDeviceId;
        }

        @Override
        public DeviceResource toResource(DeviceBaseDto device) {
            DeviceResource deviceResource = instantiateResource(device);
            ControllerLinkBuilder selfLinkBuilder = getSelfLinkBuilder(device.getId());
            addSelfLink(selfLinkBuilder, deviceResource);
            addEditLink(selfLinkBuilder, deviceResource);
            if (isRequestingDevice(device)) {
                addPostOpenAppEventLink(deviceResource);
                addAppActivityLink(deviceResource);
                addPostVpnStatusEventLink(deviceResource);
                addSslRootCertificateLink(deviceResource);
                addAppleMobileConfigLink(deviceResource);
            }
            return deviceResource;
        }

        @Override
        protected DeviceResource instantiateResource(DeviceBaseDto device) {
            return new DeviceResource(device, isRequestingDevice(device));
        }

        private boolean isRequestingDevice(DeviceBaseDto device) {
            return device.getId().equals(requestingDeviceId.orElse(null));
        }

        private ControllerLinkBuilder getSelfLinkBuilder(UUID deviceId) {
            return getDeviceLinkBuilder(userId, deviceId, requestingDeviceId);
        }

        private void addSelfLink(ControllerLinkBuilder selfLinkBuilder, DeviceResource deviceResource) {
            deviceResource.add(selfLinkBuilder.withSelfRel());
        }

        private void addEditLink(ControllerLinkBuilder selfLinkBuilder, DeviceResource deviceResource) {
            deviceResource.add(selfLinkBuilder.withRel(JsonRootRelProvider.EDIT_REL));
        }

        private void addPostOpenAppEventLink(DeviceResource deviceResource) {
            deviceResource
                    .add(DeviceController.getPostOpenAppEventLink(userId, deviceResource.getContent().getId()));
        }

        private void addAppActivityLink(DeviceResource deviceResource) {
            deviceResource.add(DeviceController.getAppActivityLink(userId, deviceResource.getContent().getId()));
        }

        private void addPostVpnStatusEventLink(DeviceResource deviceResource) {
            deviceResource
                    .add(DeviceController.getPostVpnStatusEventLink(userId, deviceResource.getContent().getId()));
        }

        private void addSslRootCertificateLink(DeviceResource deviceResource) {
            deviceResource.add(
                    linkTo(methodOn(StandardResourcesController.class).getSslRootCert()).withRel("sslRootCert"));
        }

        private void addAppleMobileConfigLink(DeviceResource deviceResource) {
            deviceResource.add(linkTo(methodOn(DeviceController.class).getAppleMobileConfig(Optional.empty(),
                    userId, deviceResource.getContent().getId())).withRel("appleMobileConfig"));
        }
    }
}