org.ligoj.app.plugin.security.fortify.FortifyPluginResource.java Source code

Java tutorial

Introduction

Here is the source code for org.ligoj.app.plugin.security.fortify.FortifyPluginResource.java

Source

/*
 * Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
 */
package org.ligoj.app.plugin.security.fortify;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.Format;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AUTH;
import org.ligoj.app.api.SubscriptionStatusWithData;
import org.ligoj.app.plugin.security.SecurityResource;
import org.ligoj.app.plugin.security.SecurityServicePlugin;
import org.ligoj.app.resource.NormalizeFormat;
import org.ligoj.app.resource.plugin.AbstractToolPluginResource;
import org.ligoj.bootstrap.core.curl.CurlCacheToken;
import org.ligoj.bootstrap.core.curl.CurlRequest;
import org.ligoj.bootstrap.core.json.InMemoryPagination;
import org.ligoj.bootstrap.core.validation.ValidationJsonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Sonar resource.
 */
@Path(FortifyPluginResource.URL)
@Service
@Produces(MediaType.APPLICATION_JSON)
public class FortifyPluginResource extends AbstractToolPluginResource implements SecurityServicePlugin {

    private static final String API_PROJECT_VERSIONS = "api/v1/projectVersions/";
    private static final String API_PROJECTS = "api/v1/projects/";
    private static final String API_TOKEN = "api/v1/auth/token";

    private static final Base64 BASE64_CODEC = new Base64(0);

    /**
     * Plug-in key.
     */
    public static final String URL = SecurityResource.SERVICE_URL + "/fortify";

    /**
     * Plug-in key.
     */
    public static final String KEY = URL.replace('/', ':').substring(1);

    /**
     * Web site URL
     */
    public static final String PARAMETER_URL = KEY + ":url";

    /**
     * Fortify project identifier.
     */
    public static final String PARAMETER_KEY = KEY + ":pkey";

    /**
     * Fortify project-version identifier.
     */
    public static final String PARAMETER_VERSION = KEY + ":version";

    /**
     * Fortify user name able to perform index.
     */
    public static final String PARAMETER_USER = KEY + ":user";

    /**
     * Fortify user password able to perform index.
     */
    public static final String PARAMETER_PASSWORD = KEY + ":password";

    @Autowired
    private InMemoryPagination inMemoryPagination;

    @Autowired
    private CurlCacheToken curlCacheToken;

    @Autowired
    private CacheManager cacheManager;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public String getKey() {
        return KEY;
    }

    @SuppressWarnings("unchecked")
    @Override
    public String getVersion(final Map<String, String> parameters) throws Exception {
        final FortifyCurlProcessor processor = newFortifyCurlProcessor(parameters);
        final String url = StringUtils.appendIfMissing(parameters.get(PARAMETER_URL), "/")
                + "api/v1/userSession/info";
        final CurlRequest request = new CurlRequest("POST", url, "{}", "Accept: application/json");
        request.setSaveResponse(true);
        processor.process(request);
        processor.close();
        final String content = ObjectUtils.defaultIfNull(request.getResponse(), "{}");
        final Map<String, ?> data = MapUtils
                .emptyIfNull((Map<String, ?>) objectMapper.readValue(content, Map.class).get("data"));
        return (String) data.get("webappVersion");
    }

    @Override
    public boolean checkStatus(final Map<String, String> parameters) throws Exception {
        return StringUtils.isNotEmpty(this.getVersion(parameters));
    }

    @Override
    public SubscriptionStatusWithData checkSubscriptionStatus(final Map<String, String> parameters)
            throws Exception {
        final SubscriptionStatusWithData nodeStatusWithData = new SubscriptionStatusWithData();
        nodeStatusWithData.put("project", validateProject(parameters));
        return nodeStatusWithData;
    }

    /**
     * Cache the API token.
     *
     * @param url
     *            Fortify URL.
     * @param user
     *            Access user.
     * @param password
     *            Access password.
     * @param processor
     *            The related processor where the fortify token will be attached to.
     * @param force
     *            When <code>true</code>, the token will be regenerated.
     *
     */
    protected String authenticate(final String url, final String user, final String password,
            final FortifyCurlProcessor processor, final boolean force) {
        final String cacheToken = url + DigestUtils.sha256Hex("##" + user + "/" + password);
        if (force) {
            // Replace the token
            final String token = getFortifyToken(url, user, password, processor);
            cacheManager.getCache("curl-tokens").put(cacheToken, token);
            return token;
        }
        return curlCacheToken.getTokenCache(FortifyProject.class, cacheToken,
                k -> getFortifyToken(url, user, password, processor), 1,
                () -> new ValidationJsonException(PARAMETER_URL, "fortify-login"));
    }

    /**
     * Prepare an authenticated connection to Fortify
     *
     * @param parameters
     *            The subscription parameters.
     * @param processor
     *            The related processor where the fortify token will be attached to.
     */
    protected void authenticate(final Map<String, String> parameters, final FortifyCurlProcessor processor,
            final boolean force) {
        // Compute the fortify token and store it in the processor
        processor.setFortifyToken(authenticate(parameters.get(PARAMETER_URL), parameters.get(PARAMETER_USER),
                StringUtils.trimToEmpty(parameters.get(PARAMETER_PASSWORD)), processor, force));
    }

    private String getFortifyToken(final String url, final String user, final String password,
            final FortifyCurlProcessor processor) {
        // Use the preempted authentication processor
        processor.setFortifyToken(null);
        final CurlRequest request = new CurlRequest("GET", StringUtils.appendIfMissing(url, "/") + API_TOKEN, null,
                "Accept:application/json", AUTH.WWW_AUTH_RESP + ":Basic "
                        + BASE64_CODEC.encodeToString((user + ':' + password).getBytes(StandardCharsets.UTF_8)));
        request.setSaveResponse(true);
        if (!processor.process(request)) {
            return null;
        }

        // Get the token.
        final Pattern pattern = Pattern.compile("\"token\"\\s*:\\s*\"([^\"]+)\"");
        final Matcher matcher = pattern.matcher(request.getResponse());
        if (!matcher.find()) {
            // Something goes wrong
            return null;
        }
        return matcher.group(1);
    }

    private FortifyCurlProcessor newFortifyCurlProcessor(final Map<String, String> parameters) {
        FortifyCurlProcessor processor = new FortifyCurlProcessor(r -> {
            if (r.getStatus() == HttpStatus.SC_UNAUTHORIZED) {
                // Authorization failed, expired token, retry once
                authenticate(parameters, (FortifyCurlProcessor) r.getProcessor(), true);
                return true;
            }
            return false;
        });
        // Check the user can log-in to Fortify
        authenticate(parameters, processor, false);
        return processor;
    }

    /**
     * Validate the project configuration.
     *
     * @param parameters
     *            The project parameters.
     * @return true if the project exists.
     */
    protected FortifyProject validateProject(final Map<String, String> parameters) throws IOException {
        final FortifyCurlProcessor processor = newFortifyCurlProcessor(parameters);
        try {
            // Check the project exists and get the name
            @SuppressWarnings("unchecked")
            final Map<String, Object> projectMap = MapUtils
                    .emptyIfNull((Map<String, Object>) getFortifyResource(parameters,
                            API_PROJECTS + parameters.get(PARAMETER_KEY) + "?fields=id,name", processor));
            if (projectMap.isEmpty()) {
                // Project does not exist
                throw new ValidationJsonException(PARAMETER_KEY, "fortify-project");
            }
            final FortifyProject project = toProject(projectMap);

            // Check the projectVersion is within this project
            final String version = parameters.get(PARAMETER_VERSION);
            @SuppressWarnings("unchecked")
            final List<Map<String, Object>> versions = (List<Map<String, Object>>) getFortifyResource(parameters,
                    API_PROJECTS + parameters.get(PARAMETER_KEY) + "/versions?fields=id,name", processor);
            project.setVersion(versions.stream().filter(map -> map.get("id").toString().equals(version)).findFirst()
                    .orElseThrow(() -> new ValidationJsonException(PARAMETER_VERSION, "fortify-version"))
                    .get("name").toString());

            // Get the project versions measures
            @SuppressWarnings("unchecked")
            final List<Map<String, Object>> measures = (List<Map<String, Object>>) getFortifyResource(parameters,
                    API_PROJECT_VERSIONS + version + "/performanceIndicatorHistories", processor);
            measures.forEach(
                    map -> project.getMeasures().put(map.get("id").toString(), map.get("value").toString()));
            return project;
        } finally {
            processor.close();
        }

    }

    @Override
    public void link(final int subscription) throws Exception {
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);

        // Validate the project key
        validateProject(parameters);
    }

    /**
     * Find the spaces matching to the given criteria.Look into space key, and space name.
     *
     * @param criteria
     *            the search criteria.
     * @param node
     *            the node to be tested with given parameters.
     * @return project name.
     */
    @GET
    @Path("{node}/{criteria}")
    @Consumes(MediaType.APPLICATION_JSON)
    public List<FortifyProject> findAllByName(@PathParam("node") final String node,
            @PathParam("criteria") final String criteria) throws IOException {
        return inMemoryPagination
                .newPage(findAll(node, "api/v1/projects?fields=id,name", criteria), PageRequest.of(0, 10))
                .getContent();
    }

    /**
     * Find all the versions of a project.
     *
     * @param node
     *            the node to be tested with given parameters.
     * @param project
     *            the project identifier
     * @param criteria
     *            the search criteria.
     * @return project name.
     */
    @GET
    @Path("versions/{node}/{project}")
    @Consumes(MediaType.APPLICATION_JSON)
    public Collection<FortifyProject> findProjectVersions(@PathParam("node") final String node,
            @PathParam("project") final String project, @PathParam("criteria") final String criteria)
            throws IOException {
        return findAll(node, API_PROJECTS + project + "/versions?fields=id,name",
                StringUtils.defaultString(criteria));
    }

    /**
     * Call a Fortify REST service to fetch items by their name.<br>
     * NOTE : process manager will be shut down.
     *
     * @param node
     *            node to query.
     * @param url
     *            query URL.
     * @param criteria
     *            Optional name to match.
     * @return Projects matching to the given criteria.
     */
    private Collection<FortifyProject> findAll(final String node, final String url, final String criteria)
            throws IOException {
        // Check the user can log-in to Fortify
        final Collection<Map<String, Object>> data = getFortifyResource(this.pvResource.getNodeParameters(node),
                url);
        final Format format = new NormalizeFormat();
        final String formatCriteria = format.format(StringUtils.trimToEmpty(criteria));

        // Filter by criteria on the project name
        final Map<Integer, FortifyProject> result = new TreeMap<>();
        data.stream().filter(item -> (format.format(item.get("name"))).contains(formatCriteria)).forEach(item -> {
            final FortifyProject entry = toProject(item);
            result.put(entry.getId(), entry);
        });
        return result.values();
    }

    /**
     * Create an authenticated request and return the data. The created processor is entirely managed : opened and
     * closed.
     */
    @SuppressWarnings("unchecked")
    private Collection<Map<String, Object>> getFortifyResource(final Map<String, String> parameters,
            final String resource) throws IOException {
        final FortifyCurlProcessor processor = newFortifyCurlProcessor(parameters);
        Collection<Map<String, Object>> result = CollectionUtils
                .emptyIfNull((List<Map<String, Object>>) getFortifyResource(parameters, resource, processor));
        processor.close();
        return result;
    }

    /**
     * Fetch given node from parameters and given URL, and return the JSON object.
     */
    private Object getFortifyResource(final Map<String, String> parameters, final String resource,
            final FortifyCurlProcessor processor) throws IOException {
        final String url = StringUtils.appendIfMissing(parameters.get(PARAMETER_URL), "/") + resource;
        final CurlRequest request = new CurlRequest("GET", url, null, "Accept: application/json");
        request.setSaveResponse(true);
        processor.process(request);

        // Parse the JSON response
        final String content = ObjectUtils.defaultIfNull(request.getResponse(), "{}");
        return objectMapper.readValue(content, Map.class).get("data");
    }

    /**
     * Map raw Fortify values to a bean
     */
    private FortifyProject toProject(final Map<String, Object> spaceRaw) {
        final FortifyProject space = new FortifyProject();
        space.setId((Integer) spaceRaw.get("id"));
        space.setName((String) spaceRaw.get("name"));
        return space;
    }

}