io.kodokojo.brick.gitlab.GitlabConfigurer.java Source code

Java tutorial

Introduction

Here is the source code for io.kodokojo.brick.gitlab.GitlabConfigurer.java

Source

/**
 * Kodo Kojo - Software factory done right
 * Copyright  2016 Kodo Kojo (infos@kodokojo.io)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package io.kodokojo.brick.gitlab;

import com.google.gson.JsonObject;
import com.squareup.okhttp.*;
import io.kodokojo.brick.BrickConfigurationException;
import io.kodokojo.brick.BrickConfigurer;
import io.kodokojo.brick.BrickConfigurerData;
import io.kodokojo.brick.BrickUrlFactory;
import io.kodokojo.model.User;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.client.OkClient;

import javax.inject.Inject;
import javax.net.ssl.*;
import java.io.IOException;
import java.net.*;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class GitlabConfigurer implements BrickConfigurer {

    private static final Logger LOGGER = LoggerFactory.getLogger(GitlabConfigurer.class);

    public static final String GITLAB_ADMIN_TOKEN_KEY = "GITLAB_ADMIN_API_TOKEN";

    public static final String GITLAB_FORCE_ENTRYPOINT_KEY = "GITLAB_FORCE_USE_DEFAULT_ENTRYPOINT";

    private static final Pattern FORM_TOKEN_PATTERN = Pattern
            .compile(".*<input type=\"hidden\" name=\"authenticity_token\" value=\"([^\"]*)\" />.*");

    private static final Pattern META_TOKEN_PATTERN = Pattern
            .compile(".*<meta name=\"csrf-token\" content=\"([^\"]*)\" />.*");

    private static final Pattern PRIVATE_TOKEN_PATTERN = Pattern.compile(
            ".*<input type=\"text\" name=\"token\" id=\"token\" value=\"([^\"]*)\" class=\"form-control\" />.*");

    private static final String SIGNIN_URL = "/users/sign_in";

    private static final String PASSWORD_URL = "/profile/password";

    private static final String PASSWORD_FORM_URL = PASSWORD_URL + "/new";

    private static final String ACCOUNT_URL = "/profile/account";

    private static final String OLD_PASSWORD = "5iveL!fe";

    private static final String ROOT_LOGIN = "root";

    public static final String GITLAB_CHANGE_FAIL_MESSAGE = "After a successful password update you will be redirected to login screen.";

    private final BrickUrlFactory brickUrlFactory;

    @Inject
    public GitlabConfigurer(BrickUrlFactory brickUrlFactory) {
        if (brickUrlFactory == null) {
            throw new IllegalArgumentException("brickUrlFactory must be defined.");
        }
        this.brickUrlFactory = brickUrlFactory;
    }

    @Override
    public BrickConfigurerData configure(BrickConfigurerData brickConfigurerData)
            throws BrickConfigurationException {
        String gitlabUrl = getGitlabEntryPoint(brickConfigurerData);
        OkHttpClient httpClient = provideDefaultOkHttpClient();
        if (signIn(httpClient, gitlabUrl, ROOT_LOGIN, OLD_PASSWORD)) {
            String token = getAuthenticityToken(httpClient, gitlabUrl + PASSWORD_FORM_URL, META_TOKEN_PATTERN);
            String newPassword = brickConfigurerData.getDefaultAdmin().getPassword();
            if (changePassword(httpClient, gitlabUrl, token, OLD_PASSWORD, newPassword)) {
                if (signIn(httpClient, gitlabUrl, ROOT_LOGIN, newPassword)) {
                    Request request = new Request.Builder().get().url(gitlabUrl + ACCOUNT_URL).build();
                    Response response = null;
                    try {
                        response = httpClient.newCall(request).execute();
                        String body = response.body().string();
                        String authenticityToken = getAuthenticityToken(body, PRIVATE_TOKEN_PATTERN);
                        if (StringUtils.isBlank(authenticityToken)) {
                            throw new BrickConfigurationException(
                                    "Unable to get private token of root account for gitlab  for project "
                                            + brickConfigurerData.getProjectName() + " on url " + gitlabUrl);
                        }
                        brickConfigurerData.addInContext(GITLAB_ADMIN_TOKEN_KEY, authenticityToken);

                        return brickConfigurerData;
                    } catch (IOException e) {
                        LOGGER.error("Unable to retrieve account page", e);
                    } finally {
                        if (response != null) {
                            try {
                                response.body().close();
                            } catch (IOException e) {
                                LOGGER.debug("Unable to close body response", e);
                            }
                        }
                    }
                } else {
                    LOGGER.error("Unable to log on Gitlab with new password");
                    throw new BrickConfigurationException("Unable to log on Gitlab with new password for project "
                            + brickConfigurerData.getProjectName() + " on url " + gitlabUrl);
                }
            } else {
                LOGGER.error("Unable to change root password on entrypoint {}", gitlabUrl);
                throw new BrickConfigurationException("Unable to change root password on Gitlab for project "
                        + brickConfigurerData.getProjectName() + " on url " + gitlabUrl);
            }
        } else {
            LOGGER.error("Unable to log as root on entrypoint {}", gitlabUrl);
            throw new BrickConfigurationException("Unable to log as root password on Gitlab for project "
                    + brickConfigurerData.getProjectName() + " on url " + gitlabUrl);
        }

        return brickConfigurerData;
    }

    @Override
    public BrickConfigurerData addUsers(BrickConfigurerData brickConfigurerData, List<User> users)
            throws BrickConfigurationException {
        if (brickConfigurerData == null) {
            throw new IllegalArgumentException("brickConfigurerData must be defined.");
        }
        if (users == null) {
            throw new IllegalArgumentException("users must be defined.");
        }

        String gitlabEntryPoint = getGitlabEntryPoint(brickConfigurerData);
        RestAdapter adapter = new RestAdapter.Builder().setEndpoint(gitlabEntryPoint)
                .setClient(new OkClient(provideDefaultOkHttpClient())).build();
        GitlabRest gitlabRest = adapter.create(GitlabRest.class);
        String privateToken = (String) brickConfigurerData.getContext().get(GITLAB_ADMIN_TOKEN_KEY);
        for (User user : users) {
            if (!createUser(gitlabRest, privateToken, user)) {
                throw new BrickConfigurationException("Unable to create user '" + user.getUsername()
                        + "' for project " + brickConfigurerData.getProjectName() + " on url " + gitlabEntryPoint);
            }
        }

        return brickConfigurerData;
    }

    private String getGitlabEntryPoint(BrickConfigurerData brickConfigurerData) {
        Boolean forceDefault = (Boolean) brickConfigurerData.getContext().get(GITLAB_FORCE_ENTRYPOINT_KEY);
        if (forceDefault != null && forceDefault) {
            return brickConfigurerData.getEntrypoint();
        }
        return "https://" + brickUrlFactory.forgeUrl(brickConfigurerData.getProjectName(),
                brickConfigurerData.getStackName(), "scm", "gitlab");
    }

    private static boolean changePassword(OkHttpClient httpClient, String gitlabUrl, String token,
            String oldPassword, String newPassword) {
        RequestBody formBody = new FormEncodingBuilder().addEncoded("utf8", "%E2%9C%93")
                .add("authenticity_token", token).add("user[current_password]", oldPassword)
                .add("user[password]", newPassword).add("user[password_confirmation]", newPassword).build();
        Request request = new Request.Builder().url(gitlabUrl + PASSWORD_URL)
                .addHeader("Content-Type", "application/x-www-form-urlencoded").post(formBody).build();
        Response response = null;
        try {
            response = httpClient.newCall(request).execute();
            String body = response.body().string();
            return response.code() == 200 && !body.contains(GITLAB_CHANGE_FAIL_MESSAGE);
        } catch (IOException e) {
            LOGGER.error("Unable to change the default password", e);
            return false;
        } finally {
            if (response != null) {
                IOUtils.closeQuietly(response.body());
            }
        }

    }

    public static boolean signIn(OkHttpClient httpClient, String gitlabUrl, String login, String password) {
        String token = getAuthenticityToken(httpClient, gitlabUrl + SIGNIN_URL, FORM_TOKEN_PATTERN);
        RequestBody formBody = new FormEncodingBuilder().addEncoded("utf8", "%E2%9C%93")
                .add("authenticity_token", token).add("user[login]", login).add("user[password]", password)
                .add("user[remember_me]", "0").build();

        Request request = new Request.Builder().url(gitlabUrl + SIGNIN_URL)
                .addHeader("Content-Type", "application/x-www-form-urlencoded").post(formBody).build();

        Call call = httpClient.newCall(request);
        Response response = null;
        try {
            response = call.execute();
            String body = response.body().string();
            return response.isSuccessful() && !body.contains("Invalid login or password.");
        } catch (IOException e) {
            LOGGER.error("Unable to change default password for Gitlab", e);
            return false;
        } finally {
            if (response != null && response.body() != null) {
                try {
                    response.body().close();
                } catch (IOException e) {
                    LOGGER.debug("Unable to close body response", e);
                }
            }
        }
    }

    private static String getAuthenticityToken(OkHttpClient httpClient, String url, Pattern pattern) {
        Request request = new Request.Builder().url(url).get().build();
        Response response = null;
        try {
            response = httpClient.newCall(request).execute();
            String bodyReponse = response.body().string();
            return getAuthenticityToken(bodyReponse, pattern);
        } catch (IOException e) {
            LOGGER.error("Unable to request " + url, e);
        } finally {
            if (response != null && response.body() != null) {
                IOUtils.closeQuietly(response.body());
            }
        }
        return null;
    }

    private boolean createUser(GitlabRest gitlabRest, String privateToken, User user) {
        Response response = null;
        int id = -1;
        try {
            JsonObject jsonObject = gitlabRest.createUser(privateToken, user.getUsername(), user.getPassword(),
                    user.getEmail(), user.getName(), "false");
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(jsonObject.toString());
            }

            id = jsonObject.getAsJsonPrimitive("id").getAsInt();
        } catch (RetrofitError e) {
            LOGGER.error("unable to complete creation of user : ", e);
        }

        if (id != -1) {
            try {
                try {
                    Thread.sleep(500); // We encount some cases where Gitlab throw a 500 internal error while post the SSH key. Test if let some time to Gitlab to add user will resolv this issue ?
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                response = gitlabRest.addSshKey(privateToken, Integer.toString(id), "SSH Key",
                        user.getSshPublicKey());
                return true;
                //return response.code() >= 200 && response.code() < 300; return a non 20X HTTP code when success to push the SSH key...
                //  Have a look on the HAproxy timeout ?

            } catch (RetrofitError e) {
                LOGGER.error("unable to add SSH keys creation of user : ", e);
            } finally {
                if (response != null) {
                    IOUtils.closeQuietly(response.body());
                }
            }
        }
        return false;
    }

    private static String getAuthenticityToken(String bodyReponse, Pattern pattern) {
        String token = "";
        Matcher matcher = pattern.matcher(bodyReponse);
        if (matcher.find()) {
            token = matcher.group(1);
        }
        return token;
    }

    public static OkHttpClient provideDefaultOkHttpClient() {
        OkHttpClient httpClient = new OkHttpClient();
        final TrustManager[] certs = new TrustManager[] { new X509TrustManager() {

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }

            @Override
            public void checkServerTrusted(final X509Certificate[] chain, final String authType)
                    throws CertificateException {
            }

            @Override
            public void checkClientTrusted(final X509Certificate[] chain, final String authType)
                    throws CertificateException {
            }
        } };

        SSLContext ctx = null;
        try {
            ctx = SSLContext.getInstance("TLS");
            ctx.init(null, certs, new SecureRandom());
        } catch (final java.security.GeneralSecurityException ex) {
            //
        }
        httpClient.setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        httpClient.setSslSocketFactory(ctx.getSocketFactory());
        CookieManager cookieManager = new CookieManager(new GitlabCookieStore(), CookiePolicy.ACCEPT_ALL);
        httpClient.setCookieHandler(cookieManager);
        httpClient.setReadTimeout(2, TimeUnit.MINUTES);
        httpClient.setConnectTimeout(1, TimeUnit.MINUTES);
        httpClient.setWriteTimeout(1, TimeUnit.MINUTES);
        return httpClient;
    }

    private static class GitlabCookieStore implements CookieStore {

        private final Map<String, HttpCookie> cache = new HashMap<>();

        @Override
        public void add(URI uri, HttpCookie cookie) {
            cache.put(cookie.getName(), cookie);
        }

        @Override
        public List<HttpCookie> get(URI uri) {
            return new ArrayList<>(cache.values());
        }

        @Override
        public List<HttpCookie> getCookies() {
            return new ArrayList<>(cache.values());
        }

        @Override
        public List<URI> getURIs() {
            return Collections.emptyList();
        }

        @Override
        public boolean remove(URI uri, HttpCookie cookie) {

            return cache.remove(cookie.getName()) != null;
        }

        @Override
        public boolean removeAll() {
            return false;
        }
    }

}