All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.connectors.aws.cert.AwsCertController Maven / Gradle / Ivy

/*
 * Copyright © 2017 VMware, Inc. All Rights Reserved.
 * SPDX-License-Identifier: BSD-2-Clause
 */

package com.vmware.connectors.aws.cert;

import com.vmware.connectors.common.payloads.request.CardRequest;
import com.vmware.connectors.common.payloads.response.*;
import com.vmware.connectors.common.utils.Async;
import com.vmware.connectors.common.utils.CardTextAccessor;
import com.vmware.connectors.common.utils.ObservableUtil;
import org.apache.commons.lang3.tuple.Pair;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.FormElement;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.AsyncRestOperations;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import rx.Observable;
import rx.Single;

import javax.validation.Valid;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.stream.Collectors;

@RestController
public class AwsCertController {

    private static final String APPROVAL_URL_PARAM = "hero_aws_cert_approval_url";

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

    private static final String ROUTING_PREFIX = "x-routing-prefix";

    private static final String APPROVE_PATH = "/api/v1/approve";

    private final String certificateApprovalHost;
    private final String certificateApprovalPath;
    private final AsyncRestOperations rest;
    private final CardTextAccessor cardTextAccessor;

    @Autowired
    public AwsCertController(
            @Value("${aws.certificate.connector.approval.host}") String certificateApprovalHost,
            @Value("${aws.certificate.connector.approval.path}") String certificateApprovalPath,
            AsyncRestOperations rest,
            CardTextAccessor cardTextAccessor
    ) {
        this.certificateApprovalHost = certificateApprovalHost.toLowerCase(Locale.US);
        this.certificateApprovalPath = certificateApprovalPath;
        this.rest = rest;
        this.cardTextAccessor = cardTextAccessor;
    }

    @PostMapping(
            path = "/cards/requests",
            produces = MediaType.APPLICATION_JSON_VALUE,
            consumes = MediaType.APPLICATION_JSON_VALUE
    )
    public Single> getCards(
            @RequestHeader(ROUTING_PREFIX) String routingPrefix,
            @Valid @RequestBody CardRequest request
    ) {
        logger.trace("getCards called, routingPrefix={}, request={}", routingPrefix, request);

        List approvalUrls = new ArrayList<>(request.getTokens("approval_urls"));

        if (CollectionUtils.isEmpty(approvalUrls)) {
            return Single.just(ResponseEntity.ok(new Cards()));
        }

        approvalUrls = validateUrls(approvalUrls);

        Collections.sort(approvalUrls);

        return getAllCardsInfo(new LinkedHashSet<>(approvalUrls))
                .filter(pair -> pair.getRight().getStatusCode().is2xxSuccessful())
                .map(this::parseCardInfoOutOfResponse)
                .reduce(
                        new Cards(),
                        (cards, info) -> appendCard(cards, info, routingPrefix)
                )
                .toSingle()
                .map(ResponseEntity::ok);
    }

    private List validateUrls(List approvalUrls) {
        return approvalUrls.stream()
                .filter(this::validateUrl)
                .collect(Collectors.toList());
    }

    private boolean validateUrl(String approvalUrl) {
        boolean isValid;
        try {
            UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(approvalUrl).build();
            isValid = verifyHost(uriComponents) && verifyPath(uriComponents);
        } catch (IllegalArgumentException e) {
            isValid = false;
        }
        if (!isValid) {
            logger.warn("Approval url was not valid: {}", approvalUrl);
        }
        return isValid;
    }

    private boolean verifyHost(UriComponents uriComponents) {
        String host = uriComponents.getHost().toLowerCase(Locale.US);
        return host.equals(certificateApprovalHost)
                || host.endsWith(certificateApprovalHost)
                && host.charAt(host.lastIndexOf(certificateApprovalHost) - 1) == '.';
    }

    private boolean verifyPath(UriComponents uriComponents) {
        return uriComponents.getPath().equals(certificateApprovalPath);
    }

    private Observable>> getAllCardsInfo(Set approvalUrls) {
        return Observable
                .from(approvalUrls)
                .flatMap(this::callForCardInfo);
    }

    private Observable>> callForCardInfo(String approvalUrl) {
        logger.trace("callForCardInfo called: approvalUrl={}", approvalUrl);

        ListenableFuture> response = rest.exchange(
                UriComponentsBuilder
                        .fromHttpUrl(approvalUrl)
                        .build()
                        .toUri(),
                HttpMethod.GET,
                HttpEntity.EMPTY,
                String.class
        );

        return Async.toSingle(response)
                .toObservable()
                // Don't let a bad AWS token skip the rest
                .onErrorResumeNext(ObservableUtil::skip400) // Expired requests will return 400 bad request
                .onErrorResumeNext(ObservableUtil::skip404) // Non-existent contexts will return 404 not found
                .map(responseEntity -> Pair.of(approvalUrl, responseEntity));
    }

    private AwsCertCardInfo parseCardInfoOutOfResponse(Pair> pair) {
        String approvalUrl = pair.getLeft();
        logger.trace("parseCardInfoOutOfResponse called: approvalUrl={}", approvalUrl);

        String html = pair.getRight().getBody();

        Document doc = Jsoup.parse(html);
        Elements elements = doc.body().children();
        Elements rows = elements.select("table > tbody > tr");

        AwsCertCardInfo info = collectInfo(rows);

        Elements formElements = elements.select("form");
        Map formParams = collectFormParams(formElements);

        // Supplement the form params with the approvalUrl so the client will tell the approve action who to POST to.
        formParams.put(APPROVAL_URL_PARAM, approvalUrl);

        info.setFormParams(formParams);

        return info;
    }

    private AwsCertCardInfo collectInfo(Elements rows) {
        AwsCertCardInfo info = new AwsCertCardInfo();

        rows.forEach(row -> {
            String label = row.child(0).text().toLowerCase(Locale.US);
            String value = row.child(1).text();

            fuzzySetDomainName(info, label, value);
            fuzzySetAccountId(info, label, value);
            fuzzySetRegionName(info, label, value);
            fuzzySetCertIdentifier(info, label, value);
        });

        return info;
    }

    private void fuzzySetDomainName(AwsCertCardInfo info, String label, String value) {
        if (label.contains("domain")) {
            info.setDomain(value);
        }
    }

    private void fuzzySetAccountId(AwsCertCardInfo info, String label, String value) {
        if (label.contains("account")) {
            info.setAccountId(value);
        }
    }

    private void fuzzySetRegionName(AwsCertCardInfo info, String label, String value) {
        if (label.contains("region")) {
            info.setRegionName(value);
        }
    }

    private void fuzzySetCertIdentifier(AwsCertCardInfo info, String label, String value) {
        if (label.contains("certificate")) {
            info.setCertIdentifier(value);
        }
    }

    private Map collectFormParams(Elements formElements) {
        Map formParams = new HashMap<>();
        List forms = formElements.forms();

        forms.forEach(
                form ->
                        form.formData().forEach(
                                kvp -> formParams.put(kvp.key(), kvp.value())
                        )
        );

        return formParams;
    }

    private Cards appendCard(Cards cards, AwsCertCardInfo info, String routingPrefix) {
        logger.trace("appendCard called: info={}, routingPrefix={}", info, routingPrefix);

        cards.getCards()
                .add(makeCard(info, routingPrefix));

        return cards;
    }

    private Card makeCard(
            AwsCertCardInfo info,
            String routingPrefix
    ) {
        logger.trace("makeCard called: info={}, routingPrefix={}", info, routingPrefix);

        CardAction.Builder approveAction = new CardAction.Builder()
                .setLabel(cardTextAccessor.getActionLabel("approve"))
                .setCompletedLabel(cardTextAccessor.getActionCompletedLabel("approve"))
                .setActionKey(CardActionKey.DIRECT)
                .setPrimary(true)
                .setRemoveCardOnCompletion(true)
                .setUrl(routingPrefix + APPROVE_PATH.substring(1))
                .setType(HttpMethod.POST);

        CardAction.Builder dismissAction = CardAction.Builder
                .dismissAction()
                .setLabel(cardTextAccessor.getActionLabel("dismiss"));

        info.getFormParams().forEach(approveAction::addRequestParam);

        return new Card.Builder()
                .setName("AwsCert") // TODO - remove this in APF-536
                .setCreationDate(OffsetDateTime.now())
                /*
                 * AWS Cert requests expire in 3 days, so anything beyond 3
                 * days of requesting the card is definitely expired.
                 *
                 * Since we don't know when the the cert was actually
                 * requested and the AWS approval page doesn't provide any
                 * information for when the cert was requested, we still might
                 * be sending cards for expired cert requests.
                 */
                .setExpirationDate(OffsetDateTime.now().plusDays(3))
                .setTemplate(routingPrefix + "templates/generic.hbs")
                .setHeader(cardTextAccessor.getHeader(), cardTextAccessor.getMessage("subtitle", info.getDomain()))
                .setBody(
                        new CardBody.Builder()
                                .setDescription(
                                        cardTextAccessor.getBody(
                                                info.getDomain(),
                                                info.getAccountId(),
                                                info.getRegionName(),
                                                info.getCertIdentifier()
                                        )
                                )
                                .addField(
                                        new CardBodyField.Builder()
                                                .setTitle(cardTextAccessor.getMessage("domain.title"))
                                                .setType(CardBodyFieldType.GENERAL)
                                                .setDescription(cardTextAccessor.getMessage("domain.description", info.getDomain()))
                                                .build()
                                )
                                .addField(
                                        new CardBodyField.Builder()
                                                .setTitle(cardTextAccessor.getMessage("accountId.title"))
                                                .setType(CardBodyFieldType.GENERAL)
                                                .setDescription(cardTextAccessor.getMessage("accountId.description", info.getAccountId()))
                                                .build()
                                )
                                .addField(
                                        new CardBodyField.Builder()
                                                .setTitle(cardTextAccessor.getMessage("regionName.title"))
                                                .setType(CardBodyFieldType.GENERAL)
                                                .setDescription(cardTextAccessor.getMessage("regionName.description", info.getRegionName()))
                                                .build()
                                )
                                .addField(
                                        new CardBodyField.Builder()
                                                .setTitle(cardTextAccessor.getMessage("certId.title"))
                                                .setType(CardBodyFieldType.GENERAL)
                                                .setDescription(cardTextAccessor.getMessage("certId.description", info.getCertIdentifier()))
                                                .build()
                                )
                                .build()
                )
                .addAction(approveAction.build())
                .addAction(dismissAction.build())
                .build();
    }

    @PostMapping(
            path = APPROVE_PATH,
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
    )
    public Single> approve(
            @RequestParam Map params
    ) {
        logger.trace("approve called: params={}", params);

        String approvalUrl = params.get(APPROVAL_URL_PARAM);

        if (!validateUrl(approvalUrl)) {
            return Single.just(ResponseEntity.badRequest().body("Bad url: " + approvalUrl));
        }

        MultiValueMap formParams = new LinkedMultiValueMap<>();

        params.entrySet()
                .stream()
                .filter(entry -> !entry.getKey().equals(APPROVAL_URL_PARAM))
                .forEach(kvp -> formParams.add(kvp.getKey(), kvp.getValue()));

        HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE);

        ListenableFuture> response = rest.exchange(
                approvalUrl,
                HttpMethod.POST,
                new HttpEntity<>(formParams, headers),
                String.class
        );

        return Async.toSingle(response);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy