
com.vmware.connectors.airwatch.AirWatchController Maven / Gradle / Ivy
The newest version!
/*
* Copyright © 2017 VMware, Inc. All Rights Reserved.
* SPDX-License-Identifier: BSD-2-Clause
*/
package com.vmware.connectors.airwatch;
import com.jayway.jsonpath.JsonPath;
import com.vmware.connectors.airwatch.config.ManagedApp;
import com.vmware.connectors.airwatch.exceptions.GbAppMapException;
import com.vmware.connectors.airwatch.exceptions.ManagedAppNotFound;
import com.vmware.connectors.airwatch.exceptions.UdidException;
import com.vmware.connectors.airwatch.exceptions.UnsupportedPlatform;
import com.vmware.connectors.airwatch.greenbox.GreenBoxApp;
import com.vmware.connectors.airwatch.greenbox.GreenBoxConnection;
import com.vmware.connectors.airwatch.service.AppConfigService;
import com.vmware.connectors.common.json.JsonDocument;
import com.vmware.connectors.common.payloads.request.CardRequest;
import com.vmware.connectors.common.payloads.response.*;
import com.vmware.connectors.common.utils.CardTextAccessor;
import com.vmware.connectors.common.utils.Reactive;
import net.minidev.json.JSONArray;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MimeType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Stream;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Created by harshas on 8/24/17.
*/
@RestController
public class AirWatchController {
private static final Logger logger = LoggerFactory.getLogger(AirWatchController.class);
private static final String AIRWATCH_AUTH_HEADER = "X-Connector-Authorization";
private static final String AIRWATCH_BASE_URL_HEADER = "X-Connector-Base-Url";
private static final String ROUTING_PREFIX = "x-routing-prefix";
private static final int AW_USER_NOT_ASSOCIATED_WITH_UDID = 1001;
private static final int AW_UDID_NOT_RESOLVED = 1002;
private static final String APP_NAME_KEY = "app_name";
private static final String UDID_KEY = "udid";
private static final String PLATFORM_KEY = "platform";
private final WebClient rest;
private final CardTextAccessor cardTextAccessor;
private final AppConfigService appConfig;
private final URI gbBaseUri;
@Autowired
public AirWatchController(WebClient rest, CardTextAccessor cardTextAccessor,
AppConfigService appConfig,
URI gbBaseUri) {
this.rest = rest;
this.cardTextAccessor = cardTextAccessor;
this.appConfig = appConfig;
this.gbBaseUri = gbBaseUri;
}
@PostMapping(path = "/cards/requests",
produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
public Mono> getCards(
@RequestHeader(name = AIRWATCH_AUTH_HEADER) String awAuth,
@RequestHeader(name = AIRWATCH_BASE_URL_HEADER) String baseUrl,
@RequestHeader(name = ROUTING_PREFIX) String routingPrefix,
Locale locale,
@Valid @RequestBody CardRequest cardRequest) {
String udid = cardRequest.getTokenSingleValue(UDID_KEY);
String clientPlatform = cardRequest.getTokenSingleValue(PLATFORM_KEY);
if (StringUtils.isAnyBlank(udid, clientPlatform)) {
logger.debug("Either device UDID or client platform is blank.");
return Mono.just(ResponseEntity.badRequest().build());
}
Set appKeywords = cardRequest.getTokens("app_keywords");
if (appKeywords == null) {
logger.debug("Request is missing app_keywords token.");
return Mono.just(ResponseEntity.badRequest().build());
}
Stream managedApps = appKeywords.stream()
.map(keyword -> appConfig.findManagedApp(keyword, clientPlatform))
.filter(Optional::isPresent)
.map(Optional::get)
.distinct();
return Flux.fromStream(managedApps)
.flatMap(app -> getCardForApp(awAuth, baseUrl, udid,
app, routingPrefix, clientPlatform, locale))
.collect(Cards::new, (cards, card) -> cards.getCards().add(card))
.map(ResponseEntity::ok);
}
@PostMapping(value = "/mdm/app/install", consumes = APPLICATION_FORM_URLENCODED_VALUE)
public Mono> installApp(
@RequestHeader(name = AIRWATCH_AUTH_HEADER) String awAuth,
@Valid InstallForm form) {
String appName = form.getAppName();
String udid = form.getUdid();
String platform = form.getPlatform();
ManagedApp app = appConfig.findManagedApp(appName, platform)
.orElseThrow(() -> new ManagedAppNotFound("Can't install " + appName + ". It is not a managed app."));
logger.debug("Found managed app. {}:{} -> {}", platform, appName, app);
String hznToken = awAuth.split("(?i)Bearer ")[1];
return getEucToken(gbBaseUri, udid, platform, hznToken)
.flatMap(eucToken -> getGbConnection(gbBaseUri, eucToken))
.flatMap(greenBoxConnection -> installGbAppByName(appName, greenBoxConnection))
.then(Mono.just(ResponseEntity.status(OK).build()));
}
@ExceptionHandler({UdidException.class, ManagedAppNotFound.class,
GbAppMapException.class, UnsupportedPlatform.class})
@ResponseStatus(BAD_REQUEST)
@ResponseBody
public Map handleException(RuntimeException e) {
logger.debug(e.getMessage());
return Collections.singletonMap("error", e.getMessage());
}
private Mono getCardForApp(String awAuth, String baseUrl, String udid,
ManagedApp app, String routingPrefix, String platform, Locale locale) {
String appName = app.getName();
String appBundle = app.getId();
logger.debug("Getting app installation status for bundleId: {} with air-watch base url: {}",
appBundle, baseUrl);
return rest.get()
.uri(baseUrl + "/deviceservices/AppInstallationStatus?Udid={udid}&BundleId={bundleId}", udid, appBundle)
.header(AUTHORIZATION, awAuth)
.retrieve()
.onStatus(HttpStatus::isError, response -> handleClientError(response, udid))
.bodyToMono(JsonDocument.class)
.flatMap(Reactive.wrapFlatMapper(body -> getCard(body, routingPrefix, appName, appBundle, udid, platform, locale)));
}
private static Mono handleClientError(ClientResponse response, String udid) {
Charset charset = response.headers().contentType()
.map(MimeType::getCharset)
.orElse(StandardCharsets.ISO_8859_1);
return response.bodyToMono(String.class)
.map(body -> {
if (!body.isEmpty()) {
Integer error = JsonPath.parse(body).read("$.Error");
if (Integer.valueOf(AW_USER_NOT_ASSOCIATED_WITH_UDID).equals(error)) {
return new UdidException("User is not associated with the UDID : " + udid);
} else if (Integer.valueOf(AW_UDID_NOT_RESOLVED).equals(error)) {
return new UdidException("Unable to resolve the UDID : " + udid);
}
}
return new WebClientResponseException("Status error",
response.statusCode().value(),
response.statusCode().getReasonPhrase(),
response.headers().asHttpHeaders(), body.getBytes(), charset);
})
.defaultIfEmpty(new WebClientResponseException("Status error",
response.statusCode().value(),
response.statusCode().getReasonPhrase(),
response.headers().asHttpHeaders(), null, charset))
.cast(Throwable.class);
}
private Mono getCard(JsonDocument installStatus, String routingPrefix,
String appName, String appBundle, String udid, String platform, Locale locale) {
Boolean isAppInstalled = Optional.ofNullable(
installStatus.read("$.IsApplicationInstalled")).orElse(true);
if (isAppInstalled) {
logger.debug("App with bundleId: {} is already installed. No card is created.", appBundle);
return Mono.empty();
}
// Create card for app install
Card.Builder cardBuilder = new Card.Builder();
CardBody.Builder cardBodyBuilder = new CardBody.Builder()
.setDescription(cardTextAccessor.getBody(locale));
CardAction.Builder appInstallActionBuilder =
getInstallActionBuilder(routingPrefix, appName, udid, platform, locale);
cardBuilder
.setName("AirWatch")
.setTemplate(routingPrefix + "templates/generic.hbs")
.setHeader(cardTextAccessor.getHeader(locale, appName))
.setBody(cardBodyBuilder.build())
.addAction(appInstallActionBuilder.build());
return Mono.just(cardBuilder.build());
}
private CardAction.Builder getInstallActionBuilder(String routingPrefix,
String appName,
String udid,
String platform,
Locale locale) {
CardAction.Builder actionBuilder = new CardAction.Builder();
actionBuilder.setLabel(cardTextAccessor.getActionLabel("installApp", locale))
.setCompletedLabel(cardTextAccessor.getActionCompletedLabel("installApp", locale))
.setActionKey(CardActionKey.DIRECT)
.setUrl(routingPrefix + "mdm/app/install")
.addRequestParam(APP_NAME_KEY, appName)
.addRequestParam(UDID_KEY, udid)
.addRequestParam(PLATFORM_KEY, platform)
.setType(HttpMethod.POST);
return actionBuilder;
}
private Mono getEucToken(URI baseUri, String udid, String platform, String hzn) {
String deviceType = platform.replaceAll("(?i)ios", "Apple");
return rest.post()
.uri(baseUri + "/catalog-portal/services/auth/eucTokens?deviceUdid={udid}&deviceType={deviceType}", udid, deviceType)
.cookie("HZN", hzn)
.retrieve()
.bodyToMono(JsonDocument.class)
.map(body -> body.read("$.eucToken"))
.cast(String.class)
.doOnEach(Reactive.wrapForItem(token -> logger.trace("Install app. Got EUC token: {}", token)));
}
private Mono getGbConnection(URI gbBaseUri, String eucToken) {
return getCsrfToken(gbBaseUri, eucToken)
.map(csrfToken -> new GreenBoxConnection(gbBaseUri, eucToken, csrfToken))
.doOnEach(Reactive.wrapForItem(gbc -> logger.trace("Install app. Got GB connection: {}", gbc)));
}
private Mono installGbAppByName(
String gbAppName, GreenBoxConnection gbSession) {
return findGbApp(gbAppName, gbSession)
.flatMap(gbApp -> installGbApp(gbApp, gbSession));
}
private Mono findGbApp(String appName, GreenBoxConnection gbSession) {
/*
* Use search API to find GreenBox app by name.
* Make sure response has only one entry.
* If by chance it finds more than one app which one should be selected to install ?
*/
logger.trace("Trying to find a unique GreenBox catalog app with name {}", appName);
return rest.get()
.uri(gbSession.getBaseUrl() + "/catalog-portal/services/api/entitlements?q={appName}", appName)
.cookie("USER_CATALOG_CONTEXT", gbSession.getEucToken())
.retrieve()
.bodyToMono(JsonDocument.class)
.map(document -> toGreenBoxApp(document, appName))
.doOnEach(Reactive.wrapForItem(gba -> logger.trace("Found GB app {} for {}", gba, appName)));
}
private GreenBoxApp toGreenBoxApp(JsonDocument document, String appName) {
JSONArray jsonArray = document.read("$._embedded.entitlements");
if (jsonArray == null) {
throw new GbAppMapException(
"vIDM catalog doesn't contain " + appName);
}
if (jsonArray.size() != 1) { //NOPMD
throw new GbAppMapException(
"Unable to map " + appName + " to a single GreenBox app");
}
return new GreenBoxApp(
document.read("$._embedded.entitlements[0].name"),
document.read("$._embedded.entitlements[0]._links.install.href"));
}
private Mono installGbApp(GreenBoxApp gbApp, GreenBoxConnection gbSession) {
/*
* It triggers the native mdm app install.
*/
logger.trace("Trigger app install for the GreenBox app : {}", gbApp.getName());
return rest.post()
.uri(gbApp.getInstallLink())
.cookie("USER_CATALOG_CONTEXT", gbSession.getEucToken())
.cookie("EUC_XSRF_TOKEN", gbSession.getCsrfToken())
.header("X-XSRF-TOKEN", gbSession.getCsrfToken())
.retrieve()
.bodyToMono(JsonDocument.class)
.map(body -> body.read("$.status"))
.cast(String.class)
.doOnEach(Reactive.wrapForItem(status ->
logger.trace("Install action status: {} for {}", status, gbApp)));
}
private Mono getCsrfToken(URI baseUri, String eucToken) {
/*
* First authenticated request to {GreenBox-Base-Url}/catalog-portal/services provides CSRF token.
* https://confluence.eng.vmware.com/display/WOR/CSRF+Protection+for+Greenbox
*/
logger.trace("getCsrfToken called: baseUri={}", baseUri.toString());
return rest.get()
.uri(baseUri + "/catalog-portal/services")
.cookie("USER_CATALOG_CONTEXT", eucToken)
.exchange()
.flatMap(Reactive::checkStatus)
.map(response -> {
ResponseCookie cookie = response.cookies().getFirst("EUC_XSRF_TOKEN");
if (cookie == null) {
throw new IllegalStateException("No cookie found!");
}
return cookie.getValue();
});
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy