io.dockstore.webservice.resources.TokenResource.java Source code

Java tutorial

Introduction

Here is the source code for io.dockstore.webservice.resources.TokenResource.java

Source

/*
 * Copyright (C) 2015 Consonance
 *
 * 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.dockstore.webservice.resources;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.eclipse.egit.github.core.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;

import io.dockstore.webservice.CustomWebApplicationException;
import io.dockstore.webservice.Helper;
import io.dockstore.webservice.core.Token;
import io.dockstore.webservice.core.TokenType;
import io.dockstore.webservice.core.User;
import io.dockstore.webservice.jdbi.TokenDAO;
import io.dockstore.webservice.jdbi.UserDAO;
import io.dropwizard.auth.Auth;
import io.dropwizard.auth.CachingAuthenticator;
import io.dropwizard.hibernate.UnitOfWork;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

/**
 * The githubToken resource handles operations with tokens. Tokens are needed to talk with the quay.io and github APIs. In addition, they
 * will be needed to pull down docker containers that are requested by users.
 *
 * @author dyuen
 */
@Path("/auth/tokens")
@Api(value = "/auth/tokens", tags = "tokens")
@Produces(MediaType.APPLICATION_JSON)
public class TokenResource {
    private final TokenDAO tokenDAO;
    private final UserDAO userDAO;
    private static final String GIT_URL = "https://github.com/";
    private static final String QUAY_URL = "https://quay.io/api/v1/";
    private static final String BITBUCKET_URL = "https://bitbucket.org/";
    private final String githubClientID;
    private final String githubClientSecret;
    private final String bitbucketClientID;
    private final String bitbucketClientSecret;
    private final HttpClient client;

    private static final int MAX_ITERATIONS = 5;

    private static final Logger LOG = LoggerFactory.getLogger(TokenResource.class);
    private final CachingAuthenticator<String, Token> cachingAuthenticator;

    @SuppressWarnings("checkstyle:parameternumber")
    public TokenResource(TokenDAO tokenDAO, UserDAO enduserDAO, String githubClientID, String githubClientSecret,
            String bitbucketClientID, String bitbucketClientSecret, HttpClient client,
            CachingAuthenticator<String, Token> cachingAuthenticator) {
        this.tokenDAO = tokenDAO;
        userDAO = enduserDAO;
        this.githubClientID = githubClientID;
        this.githubClientSecret = githubClientSecret;
        this.bitbucketClientID = bitbucketClientID;
        this.bitbucketClientSecret = bitbucketClientSecret;
        this.client = client;
        this.cachingAuthenticator = cachingAuthenticator;
    }

    @GET
    @Timed
    @UnitOfWork
    @ApiOperation(value = "List all known tokens", notes = "List all tokens. Admin Only.", response = Token.class, responseContainer = "List")
    public List<Token> listTokens(@ApiParam(hidden = true) @Auth Token authToken) {
        User user = userDAO.findById(authToken.getUserId());
        Helper.checkUser(user);

        return tokenDAO.findAll();
    }

    @GET
    @Path("/{tokenId}")
    @Timed
    @UnitOfWork
    @ApiOperation(value = "Get a specific token by id", notes = "Get a specific token by id", response = Token.class)
    @ApiResponses({ @ApiResponse(code = HttpStatus.SC_BAD_REQUEST, message = "Invalid ID supplied"),
            @ApiResponse(code = HttpStatus.SC_NOT_FOUND, message = "Token not found") })
    public Token listToken(@ApiParam(hidden = true) @Auth Token authToken,
            @ApiParam("ID of token to return") @PathParam("tokenId") Long tokenId) {
        User user = userDAO.findById(authToken.getUserId());
        Token t = tokenDAO.findById(tokenId);
        Helper.checkUser(user, t.getUserId());

        return t;
    }

    @GET
    @Timed
    @UnitOfWork
    @Path("/quay.io")
    @ApiOperation(value = "Add a new quay IO token", notes = "This is used as part of the OAuth 2 web flow. "
            + "Once a user has approved permissions for Collaboratory"
            + "Their browser will load the redirect URI which should resolve here", response = Token.class)
    public Token addQuayToken(@ApiParam(hidden = true) @Auth Token authToken,
            @QueryParam("access_token") String accessToken) {
        if (accessToken.isEmpty()) {
            throw new CustomWebApplicationException("Please provide an access token.", HttpStatus.SC_BAD_REQUEST);
        }

        User user = userDAO.findById(authToken.getUserId());

        String url = QUAY_URL + "user/";
        Optional<String> asString = ResourceUtilities.asString(url, accessToken, client);

        String username = null;
        if (asString.isPresent()) {
            LOG.info("RESOURCE CALL: {}", url);

            String response = asString.get();
            Gson gson = new Gson();
            Map<String, String> map = new HashMap<>();
            map = (Map<String, String>) gson.fromJson(response, map.getClass());

            username = map.get("username");
            LOG.info("Username: {}", username);
        }

        if (user != null) {
            List<Token> tokens = tokenDAO.findQuayByUserId(user.getId());

            if (tokens.isEmpty()) {
                Token token = new Token();
                token.setTokenSource(TokenType.QUAY_IO.toString());
                token.setContent(accessToken);
                token.setUserId(user.getId());
                if (username != null) {
                    token.setUsername(username);
                } else {
                    LOG.info("Quay.io tokenusername is null, did not create token");
                    throw new CustomWebApplicationException("Username not found from resource call " + url,
                            HttpStatus.SC_CONFLICT);
                }
                long create = tokenDAO.create(token);
                LOG.info("Quay token created for {}", user.getUsername());
                return tokenDAO.findById(create);
            } else {
                LOG.info("Quay token already exists for {}", user.getUsername());
                throw new CustomWebApplicationException("Quay token already exists for " + user.getUsername(),
                        HttpStatus.SC_CONFLICT);
            }
        } else {
            LOG.info("Could not find user");
            throw new CustomWebApplicationException("User not found", HttpStatus.SC_CONFLICT);
        }
    }

    @DELETE
    @Path("/{tokenId}")
    @UnitOfWork
    @ApiOperation("Deletes a token")
    @ApiResponses(@ApiResponse(code = HttpStatus.SC_BAD_REQUEST, message = "Invalid token value"))
    public Response deleteToken(@ApiParam(hidden = true) @Auth Token authToken,
            @ApiParam(value = "Token id to delete", required = true) @PathParam("tokenId") Long tokenId) {
        User user = userDAO.findById(authToken.getUserId());
        Token token = tokenDAO.findById(tokenId);
        Helper.checkUser(user, token.getUserId());

        // invalidate cache now that we're deleting the token
        cachingAuthenticator.invalidate(token.getContent());

        tokenDAO.delete(token);

        token = tokenDAO.findById(tokenId);
        if (token == null) {
            return Response.ok().build();
        } else {
            return Response.serverError().build();
        }
    }

    @GET
    @Timed
    @UnitOfWork
    @Path("/github.com")
    @ApiOperation(value = "Add a new github.com token, used by quay.io redirect", notes = "This is used as part of the OAuth 2 web flow. "
            + "Once a user has approved permissions for Collaboratory"
            + "Their browser will load the redirect URI which should resolve here", response = Token.class)
    public Token addGithubToken(@QueryParam("code") String code) {
        String accessToken;
        String error;
        int count = MAX_ITERATIONS;
        while (true) {
            Optional<String> asString = ResourceUtilities.asString(GIT_URL + "login/oauth/access_token?code=" + code
                    + "&client_id=" + githubClientID + "&client_secret=" + githubClientSecret, null, client);

            if (asString.isPresent()) {
                Map<String, String> split = Splitter.on('&').trimResults().withKeyValueSeparator("=")
                        .split(asString.get());
                accessToken = split.get("access_token");
                error = split.get("error");
            } else {
                throw new CustomWebApplicationException("Could not retrieve github.com token based on code",
                        HttpStatus.SC_BAD_REQUEST);
            }

            if (error != null && "bad_verification_code".equals(error)) {
                LOG.info("ERROR: {}", error);
                if (--count == 0) {
                    throw new CustomWebApplicationException("Could not retrieve github.com token based on code",
                            HttpStatus.SC_BAD_REQUEST);
                } else {
                    LOG.info("trying again...");
                }
            } else if (accessToken != null && !accessToken.isEmpty()) {
                LOG.info("Successfully recieved accessToken: {}", accessToken);
                break;
            } else {
                LOG.info("Retrieving accessToken was unsuccessful");
                throw new CustomWebApplicationException("Could not retrieve github.com token based on code",
                        HttpStatus.SC_BAD_REQUEST);
            }
        }

        GitHubClient githubClient = new GitHubClient();
        githubClient.setOAuth2Token(accessToken);
        long userID;
        String githubLogin;
        Token dockstoreToken = null;
        Token githubToken = null;
        try {
            UserService uService = new UserService(githubClient);
            org.eclipse.egit.github.core.User githubUser = uService.getUser();

            githubLogin = githubUser.getLogin();
        } catch (IOException ex) {
            throw new CustomWebApplicationException("Token ignored due to IOException", HttpStatus.SC_CONFLICT);
        }

        User user = userDAO.findByUsername(githubLogin);
        if (user == null) {
            user = new User();
            user.setUsername(githubLogin);
            userID = userDAO.create(user);

            // CREATE DOCKSTORE TOKEN
            final Random random = new Random();
            final int bufferLength = 1024;
            final byte[] buffer = new byte[bufferLength];
            random.nextBytes(buffer);
            String randomString = BaseEncoding.base64Url().omitPadding().encode(buffer);
            final String dockstoreAccessToken = Hashing.sha256()
                    .hashString(githubLogin + randomString, Charsets.UTF_8).toString();

            dockstoreToken = new Token();
            dockstoreToken.setTokenSource(TokenType.DOCKSTORE.toString());
            dockstoreToken.setContent(dockstoreAccessToken);
            dockstoreToken.setUserId(userID);
            dockstoreToken.setUsername(githubLogin);
            long dockstoreTokenId = tokenDAO.create(dockstoreToken);
            dockstoreToken = tokenDAO.findById(dockstoreTokenId);

        } else {
            userID = user.getId();
            List<Token> tokens = tokenDAO.findDockstoreByUserId(userID);
            if (!tokens.isEmpty()) {
                dockstoreToken = tokens.get(0);
            }

            tokens = tokenDAO.findGithubByUserId(userID);
            if (!tokens.isEmpty()) {
                githubToken = tokens.get(0);
            }
        }

        if (dockstoreToken == null) {
            LOG.info("Could not find user's dockstore token. Making new one...");
            final Random random = new Random();
            final int bufferLength = 1024;
            final byte[] buffer = new byte[bufferLength];
            random.nextBytes(buffer);
            String randomString = BaseEncoding.base64Url().omitPadding().encode(buffer);
            final String dockstoreAccessToken = Hashing.sha256()
                    .hashString(githubLogin + randomString, Charsets.UTF_8).toString();

            dockstoreToken = new Token();
            dockstoreToken.setTokenSource(TokenType.DOCKSTORE.toString());
            dockstoreToken.setContent(dockstoreAccessToken);
            dockstoreToken.setUserId(userID);
            dockstoreToken.setUsername(githubLogin);
            long dockstoreTokenId = tokenDAO.create(dockstoreToken);
            dockstoreToken = tokenDAO.findById(dockstoreTokenId);
        }

        if (githubToken == null) {
            LOG.info("Could not find user's github token. Making new one...");
            // CREATE GITHUB TOKEN
            githubToken = new Token();
            githubToken.setTokenSource(TokenType.GITHUB_COM.toString());
            githubToken.setContent(accessToken);
            githubToken.setUserId(userID);
            githubToken.setUsername(githubLogin);
            tokenDAO.create(githubToken);
            LOG.info("Github token created for {}", githubLogin);
        }

        return dockstoreToken;
    }

    @GET
    @Timed
    @UnitOfWork
    @Path("/bitbucket.org")
    @ApiOperation(value = "Add a new bitbucket.org token, used by quay.io redirect", notes = "This is used as part of the OAuth 2 web flow. "
            + "Once a user has approved permissions for Collaboratory"
            + "Their browser will load the redirect URI which should resolve here", response = Token.class)
    public Token addBitbucketToken(@ApiParam(hidden = true) @Auth Token authToken, @QueryParam("code") String code)
            throws UnsupportedEncodingException {
        if (code.isEmpty()) {
            throw new CustomWebApplicationException("Please provide an access code", HttpStatus.SC_BAD_REQUEST);
        }

        User user = userDAO.findById(authToken.getUserId());

        String url = BITBUCKET_URL + "site/oauth2/access_token";

        Optional<String> asString = ResourceUtilities.bitbucketPost(url, null, client, bitbucketClientID,
                bitbucketClientSecret, "grant_type=authorization_code&code=" + code);
        String accessToken;
        String refreshToken;
        if (asString.isPresent()) {
            LOG.info("RESOURCE CALL: {}", url);
            String json = asString.get();
            LOG.info(json);

            Gson gson = new Gson();
            Map<String, String> map = new HashMap<>();
            map = (Map<String, String>) gson.fromJson(json, map.getClass());

            accessToken = map.get("access_token");
            refreshToken = map.get("refresh_token");
        } else {
            throw new CustomWebApplicationException("Could not retrieve bitbucket.org token based on code",
                    HttpStatus.SC_BAD_REQUEST);
        }

        String username = null;

        url = BITBUCKET_URL + "api/2.0/users/victoroicr";
        Optional<String> asString2 = ResourceUtilities.asString(url, accessToken, client);

        if (asString2.isPresent()) {
            LOG.info("RESOURCE CALL: {}", url);

            String response = asString2.get();
            Gson gson = new Gson();
            Map<String, String> map = new HashMap<>();
            map = (Map<String, String>) gson.fromJson(response, map.getClass());

            username = map.get("username");
            LOG.info("Username: {}", username);
        }

        if (user != null) {
            List<Token> tokens = tokenDAO.findBitbucketByUserId(user.getId());

            if (tokens.isEmpty()) {
                Token token = new Token();
                token.setTokenSource(TokenType.BITBUCKET_ORG.toString());
                token.setContent(accessToken);
                token.setRefreshToken(refreshToken);
                token.setUserId(user.getId());
                if (username != null) {
                    token.setUsername(username);
                } else {
                    LOG.info("Bitbucket.org token username is null, did not create token");
                    throw new CustomWebApplicationException("Username not found from resource call " + url,
                            HttpStatus.SC_CONFLICT);
                }
                long create = tokenDAO.create(token);
                LOG.info("Bitbucket token created for {}", user.getUsername());
                return tokenDAO.findById(create);
            } else {
                LOG.info("Bitbucket token already exists for {}", user.getUsername());
                throw new CustomWebApplicationException("Bitbucket token already exists for " + user.getUsername(),
                        HttpStatus.SC_CONFLICT);
            }
        } else {
            LOG.info("Could not find user");
            throw new CustomWebApplicationException("User not found", HttpStatus.SC_CONFLICT);
        }
    }

    @GET
    @Timed
    @UnitOfWork
    @Path("/bitbucket.org/refresh")
    @ApiOperation(value = "Refresh Bitbucket token", notes = "The Bitbucket token expire in one hour. When this happens you'll get 401 responses", response = Token.class)
    public Token refreshBitbucketToken(@ApiParam(hidden = true) @Auth Token authToken) {
        List<Token> tokens = tokenDAO.findBitbucketByUserId(authToken.getUserId());

        if (tokens.isEmpty()) {
            throw new CustomWebApplicationException("User's Bitbucket token not found.", HttpStatus.SC_BAD_REQUEST);
        }

        Token bitbucketToken = tokens.get(0);

        return Helper.refreshBitbucketToken(bitbucketToken, client, tokenDAO, bitbucketClientID,
                bitbucketClientSecret);
    }
}