diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/APIService.java b/src/main/java/org/eclipse/foundation/gerrit/validation/APIService.java index bd17d7bb5a85bd07ec6cb421701f454a27058fcf..b3b94bbc7e17e404bded8e609bf03ce58359ca3f 100644 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/APIService.java +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/APIService.java @@ -10,29 +10,18 @@ */ package org.eclipse.foundation.gerrit.validation; -import java.util.List; import java.util.concurrent.CompletableFuture; import okhttp3.HttpUrl; import retrofit2.Response; -import retrofit2.http.GET; -import retrofit2.http.Path; -import retrofit2.http.Query; +import retrofit2.http.Body; +import retrofit2.http.POST; interface APIService { static final HttpUrl BASE_URL = HttpUrl.get("https://api.eclipse.org/"); - @GET("/account/profile") - CompletableFuture<Response<List<UserAccount>>> search( - @Query("uid") Integer uid, @Query("name") String name, @Query("mail") String mail); - - @GET("/account/profile/{name}") - CompletableFuture<Response<UserAccount>> search(@Path("name") String name); - - @GET("/account/profile/{name}/eca") - CompletableFuture<Response<ECA>> eca(@Path("name") String name); - - @GET("/bots") - CompletableFuture<Response<List<Bot>>> bots(@Query("q") String query); + @POST("/git/eca") + CompletableFuture<Response<ValidationResponse>> validate( + @Body ValidationRequest request); } diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/AccessToken.java b/src/main/java/org/eclipse/foundation/gerrit/validation/AccessToken.java deleted file mode 100644 index d719917d6c2fde82e685bd31eddeb61810ef1ca9..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/AccessToken.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * ******************************************************************* Copyright (c) 2019 Eclipse - * Foundation and others. - * - * <p>This program and the accompanying materials are made available under the terms of the Eclipse - * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * <p>SPDX-License-Identifier: EPL-2.0 - * ******************************************************************** - */ -package org.eclipse.foundation.gerrit.validation; - -import com.google.auto.value.AutoValue; -import com.squareup.moshi.Json; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -@AutoValue -abstract class AccessToken { - - @Json(name = "access_token") - abstract String accessToken(); - - @Json(name = "expires_in") - abstract int expiresInSeconds(); - - @Json(name = "token_type") - abstract String tokenType(); - - abstract String scope(); - - String bearerCredentials() { - return "Bearer " + accessToken(); - } - - // TODO: make package-private - public static JsonAdapter<AccessToken> jsonAdapter(Moshi moshi) { - return new AutoValue_AccessToken.MoshiJsonAdapter(moshi); - } - - abstract Builder toBuilder(); - - static Builder builder() { - return new AutoValue_AccessToken.Builder(); - } - - @AutoValue.Builder - abstract static class Builder { - abstract Builder accessToken(String accessToken); - - abstract Builder expiresInSeconds(int expireInSeconds); - - abstract Builder tokenType(String tokenType); - - abstract Builder scope(String scope); - - abstract AccessToken build(); - } -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/AccessTokenProvider.java b/src/main/java/org/eclipse/foundation/gerrit/validation/AccessTokenProvider.java deleted file mode 100644 index 00b021311c210c2f0f98d3e9d5ac59486a8e4ee6..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/AccessTokenProvider.java +++ /dev/null @@ -1,64 +0,0 @@ -/** - * ******************************************************************* Copyright (c) 2019 Eclipse - * Foundation and others. - * - * <p>This program and the accompanying materials are made available under the terms of the Eclipse - * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * <p>SPDX-License-Identifier: EPL-2.0 - * ******************************************************************** - */ -package org.eclipse.foundation.gerrit.validation; - -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class AccessTokenProvider { - - private final AccountsService accountsService; - private final String grantType; - private final String clientId; - private final String clientSecret; - private final String scope; - private Optional<AccessToken> cachedToken; - - private static final Logger log = LoggerFactory.getLogger(AccessTokenProvider.class); - - public AccessTokenProvider( - AccountsService accountsService, - String grantType, - String clientId, - String clientSecret, - String scope) { - this.accountsService = accountsService; - this.grantType = grantType; - this.clientId = clientId; - this.clientSecret = clientSecret; - this.scope = scope; - this.cachedToken = Optional.empty(); - } - - Optional<AccessToken> refreshToken() { - this.cachedToken = getTokenFromServer(); - return token(); - } - - Optional<AccessToken> token() { - return this.cachedToken; - } - - private Optional<AccessToken> getTokenFromServer() { - log.info("Getting new token from server " + AccountsService.BASE_URL); - CompletableFuture<AccessToken> credentials = - this.accountsService.postCredentials( - this.grantType, this.clientId, this.clientSecret, this.scope); - try { - return Optional.ofNullable(credentials.get()); - } catch (InterruptedException | ExecutionException e) { - return Optional.empty(); - } - } -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/AccountsService.java b/src/main/java/org/eclipse/foundation/gerrit/validation/AccountsService.java deleted file mode 100644 index 8cffc8e1b174047e4fcff449c910d7a0c81d1b4e..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/AccountsService.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * ******************************************************************* Copyright (c) 2019 Eclipse - * Foundation and others. - * - * <p>This program and the accompanying materials are made available under the terms of the Eclipse - * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * <p>SPDX-License-Identifier: EPL-2.0 - * ******************************************************************** - */ -package org.eclipse.foundation.gerrit.validation; - -import java.util.concurrent.CompletableFuture; -import okhttp3.HttpUrl; -import retrofit2.http.Field; -import retrofit2.http.FormUrlEncoded; -import retrofit2.http.POST; - -interface AccountsService { - - static final HttpUrl BASE_URL = HttpUrl.get("https://accounts.eclipse.org/"); - - @FormUrlEncoded - @POST("oauth2/token") - CompletableFuture<AccessToken> postCredentials( - @Field("grant_type") String grantType, - @Field("client_id") String clientId, - @Field("client_secret") String clientSecret, - @Field("scope") String scope); -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/Bot.java b/src/main/java/org/eclipse/foundation/gerrit/validation/Bot.java deleted file mode 100644 index d2ad5bfa297bbd3b8764d5cdce0df441aa527cd3..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/Bot.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.eclipse.foundation.gerrit.validation; - -import javax.annotation.Nullable; - -import com.google.auto.value.AutoValue; -import com.squareup.moshi.Json; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -@AutoValue -public abstract class Bot { - - public abstract int id(); - public abstract String projectId(); - public abstract String username(); - @Nullable - public abstract String email(); - - @Nullable - @Json(name = "github.com") - public abstract BotAccount gitHub(); - @Nullable - @Json(name = "github.com-dependabot") - public abstract BotAccount dependabot(); - @Nullable - @Json(name = "oss.sonatype.org") - public abstract BotAccount ossrh(); - @Nullable - @Json(name = "docker.com") - public abstract BotAccount dockerHub(); - - public static JsonAdapter<Bot> jsonAdapter(Moshi moshi) { - return new AutoValue_Bot.MoshiJsonAdapter(moshi); - } -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/BotAccount.java b/src/main/java/org/eclipse/foundation/gerrit/validation/BotAccount.java deleted file mode 100644 index c8326bd2724649bfde50784b484b51b15d6b39c7..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/BotAccount.java +++ /dev/null @@ -1,34 +0,0 @@ -/* Copyright (c) 2019 Eclipse Foundation and others. - * This program and the accompanying materials are made available - * under the terms of the Eclipse Public License 2.0 - * which is available at http://www.eclipse.org/legal/epl-v20.html, - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.foundation.gerrit.validation; - -import java.util.Objects; -import java.util.regex.Pattern; - -import javax.annotation.Nullable; - -import com.google.auto.value.AutoValue; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -@AutoValue -public abstract class BotAccount { - - @Nullable - public abstract String username(); - @Nullable - public abstract String email(); - - public boolean matches(Pattern pattern) { - return pattern.matcher(username()).matches() || - (Objects.nonNull(email()) && pattern.matcher(email()).matches()); - } - - public static JsonAdapter<BotAccount> jsonAdapter(Moshi moshi) { - return new AutoValue_BotAccount.MoshiJsonAdapter(moshi); - } -} \ No newline at end of file diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/Commit.java b/src/main/java/org/eclipse/foundation/gerrit/validation/Commit.java new file mode 100644 index 0000000000000000000000000000000000000000..84efb8f837fac4255a5d1b43ab91f401f8da34ef --- /dev/null +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/Commit.java @@ -0,0 +1,68 @@ +/** + * ***************************************************************************** Copyright (C) 2020 + * Eclipse Foundation + * + * <p>This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * <p>SPDX-License-Identifier: EPL-2.0 + * **************************************************************************** + */ +package org.eclipse.foundation.gerrit.validation; + +import java.util.List; + +import org.eclipse.foundation.gerrit.validation.GitUser.Builder; + +import com.google.auto.value.AutoValue; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +/** + * Represents a Git commit with basic data and metadata about the revision. + * + * @author Martin Lowe + */ +@AutoValue +public abstract class Commit { + public abstract String hash(); + + public abstract String subject(); + + public abstract String body(); + + public abstract List<String> parents(); + + public abstract GitUser author(); + + public abstract GitUser committer(); + + public abstract boolean head(); + + public static JsonAdapter<Commit> jsonAdapter(Moshi moshi) { + return new AutoValue_Commit.MoshiJsonAdapter(moshi); + } + + static Builder builder() { + return new AutoValue_Commit.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + public abstract Builder hash(String hash); + + public abstract Builder subject(String subject); + + public abstract Builder body(String body); + + public abstract Builder parents(List<String> parents); + + public abstract Builder author(GitUser author); + + public abstract Builder committer(GitUser committer); + + public abstract Builder head(boolean head); + + abstract Commit build(); + } +} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/CommitStatus.java b/src/main/java/org/eclipse/foundation/gerrit/validation/CommitStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..a4d67f6a4542d05997aa3c37bab8fa82b90bb70a --- /dev/null +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/CommitStatus.java @@ -0,0 +1,51 @@ +/** + * ***************************************************************************** Copyright (C) 2020 + * Eclipse Foundation + * + * <p>This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * <p>SPDX-License-Identifier: EPL-2.0 + * **************************************************************************** + */ +package org.eclipse.foundation.gerrit.validation; + +import java.util.List; + +import com.google.auto.value.AutoValue; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +/** + * Contains information generated about a commit that was submitted for validation to the API. + * + * @author Martin Lowe + */ +@AutoValue +public abstract class CommitStatus { + public abstract List<CommitStatusMessage> messages(); + + public abstract List<CommitStatusMessage> warnings(); + + public abstract List<CommitStatusMessage> errors(); + + public static JsonAdapter<CommitStatus> jsonAdapter(Moshi moshi) { + return new AutoValue_CommitStatus.MoshiJsonAdapter(moshi); + } + + /** + * Represents a message with an associated error or success status code. + * + * @author Martin Lowe + */ + @AutoValue + public abstract static class CommitStatusMessage { + public abstract int code(); + + public abstract String message(); + + public static JsonAdapter<CommitStatusMessage> jsonAdapter(Moshi moshi) { + return new AutoValue_CommitStatus_CommitStatusMessage.MoshiJsonAdapter(moshi); + } + } +} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/ECA.java b/src/main/java/org/eclipse/foundation/gerrit/validation/ECA.java deleted file mode 100644 index 4075622f72ff3d3d1248be906eaca55b5214cc91..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/ECA.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * ******************************************************************* Copyright (c) 2019 Eclipse - * Foundation and others. - * - * <p>This program and the accompanying materials are made available under the terms of the Eclipse - * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * <p>SPDX-License-Identifier: EPL-2.0 - * ******************************************************************** - */ -package org.eclipse.foundation.gerrit.validation; - -import com.google.auto.value.AutoValue; -import com.squareup.moshi.Json; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -@AutoValue -abstract class ECA { - - abstract boolean signed(); - - @Json(name = "can_contribute_spec_project") - abstract boolean canContributeToSpecProject(); - - // TODO: make package-private - public static JsonAdapter<ECA> jsonAdapter(Moshi moshi) { - return new AutoValue_ECA.MoshiJsonAdapter(moshi); - } - - static Builder builder() { - return new AutoValue_ECA.Builder(); - } - - @AutoValue.Builder - abstract static class Builder { - abstract Builder signed(boolean signed); - - abstract Builder canContributeToSpecProject(boolean canContributeToSpecProject); - - abstract ECA build(); - } -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/EclipseCommitValidationListener.java b/src/main/java/org/eclipse/foundation/gerrit/validation/EclipseCommitValidationListener.java index 72d90f4b1eff4910674afb856508379e2839d2ef..60ee1d4fb4362ccb21b11f18d6b862218ee984ca 100644 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/EclipseCommitValidationListener.java +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/EclipseCommitValidationListener.java @@ -9,38 +9,36 @@ */ package org.eclipse.foundation.gerrit.validation; +import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.HashSet; +import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import java.util.function.Predicate; import java.util.stream.Collectors; +import org.eclipse.foundation.gerrit.validation.CommitStatus.CommitStatusMessage; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.google.gerrit.extensions.annotations.Listen; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.account.externalids.ExternalId; -import com.google.gerrit.server.account.externalids.ExternalIds; -import com.google.gerrit.server.config.PluginConfig; -import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.events.CommitReceivedEvent; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.validators.CommitValidationException; import com.google.gerrit.server.git.validators.CommitValidationListener; import com.google.gerrit.server.git.validators.CommitValidationMessage; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonEncodingException; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.revwalk.RevCommit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import okhttp3.ResponseBody; +import okio.BufferedSource; import retrofit2.Response; /** @@ -58,39 +56,24 @@ import retrofit2.Response; @Listen @Singleton public class EclipseCommitValidationListener implements CommitValidationListener { - private static final String PLUGIN_NAME = "eclipse-eca-validation"; - - private static final String CFG__GRANT_TYPE = "grantType"; - private static final String CFG__GRANT_TYPE_DEFAULT = "client_credentials"; - - private static final String CFG__SCOPE = "scope"; - private static final String CFG__SCOPE_DEFAULT = "eclipsefdn_view_all_profiles"; - - private static final String CFG__CLIENT_SECRET = "clientSecret"; - private static final String CFG__CLIENT_ID = "clientId"; - private static final Logger log = LoggerFactory.getLogger(EclipseCommitValidationListener.class); private static final String ECA_DOCUMENTATION = "Please see http://wiki.eclipse.org/ECA"; - private final ExternalIds externalIds; - private final IdentifiedUser.GenericFactory factory; + private final GitRepositoryManager repoManager; private final APIService apiService; + private final JsonAdapter<ValidationResponse> responseAdapter; @Inject - public EclipseCommitValidationListener( - ExternalIds externalIds, - IdentifiedUser.GenericFactory factory, - PluginConfigFactory cfgFactory) { - this.externalIds = externalIds; - this.factory = factory; - PluginConfig config = cfgFactory.getFromGerritConfig(PLUGIN_NAME, true); - RetrofitFactory retrofitFactory = - new RetrofitFactory( - config.getString(CFG__GRANT_TYPE, CFG__GRANT_TYPE_DEFAULT), - config.getString(CFG__CLIENT_ID), - config.getString(CFG__CLIENT_SECRET), - config.getString(CFG__SCOPE, CFG__SCOPE_DEFAULT)); + public EclipseCommitValidationListener(GitRepositoryManager repoManager) { + this.repoManager = repoManager; + RetrofitFactory retrofitFactory = new RetrofitFactory(); this.apiService = retrofitFactory.newService(APIService.BASE_URL, APIService.class); + Optional<JsonAdapter<ValidationResponse>> adapter = + retrofitFactory.adapter(ValidationResponse.class); + if (adapter.isEmpty()) { + throw new IllegalStateException("Cannot process validation responses, not continuing"); + } + this.responseAdapter = adapter.get(); } /** @@ -99,11 +82,27 @@ public class EclipseCommitValidationListener implements CommitValidationListener @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { + List<CommitValidationMessage> messages = new ArrayList<>(); + List<String> errors = new ArrayList<>(); + + // create the request container + ValidationRequest.Builder req = ValidationRequest.builder(); + req.provider("gerrit"); + // get the disk location for the project and set to the request + try (Repository repo = this.repoManager.openRepository(receiveEvent.project.getNameKey())) { + File indexFile = repo.getDirectory(); + String projLoc = indexFile.getAbsolutePath(); + req.repoUrl(projLoc); + } catch (IOException e) { + log.error(e.getMessage(), e); + throw new CommitValidationException(e.getMessage()); + } + // retrieve information about the current commit RevCommit commit = receiveEvent.commit; PersonIdent authorIdent = commit.getAuthorIdent(); + PersonIdent committerIdent = commit.getCommitterIdent(); - List<CommitValidationMessage> messages = new ArrayList<>(); addSeparatorLine(messages); messages.add( new CommitValidationMessage( @@ -114,33 +113,50 @@ public class EclipseCommitValidationListener implements CommitValidationListener "Authored by: %1$s <%2$s>", authorIdent.getName(), authorIdent.getEmailAddress()), false)); addEmptyLine(messages); - - /* - * Retrieve the authors Gerrit identity if it exists - */ - Optional<IdentifiedUser> author = identifyUser(authorIdent); - if (!author.isPresent()) { - messages.add( - new CommitValidationMessage("The author does not have a Gerrit account.", false)); - } - - List<String> errors = new ArrayList<>(); - if (hasCurrentAgreement(authorIdent, author)) { - messages.add( - new CommitValidationMessage( - "The author has a current Eclipse Contributor Agreement (ECA) on file.", false)); - } else { - if (isABot(authorIdent, author)) { - messages.add(new CommitValidationMessage("The author is a registered bot and does not need an ECA.", false)); + // update the commit list for the request to contain the current request + req.commits(Arrays.asList(getRequestCommit(commit, authorIdent, committerIdent))); + // send the request and await the response from the API + CompletableFuture<Response<ValidationResponse>> futureResponse = + this.apiService.validate(req.build()); + try { + Response<ValidationResponse> rawResponse = futureResponse.get(); + ValidationResponse response; + // handle error responses (okhttp doesn't assume error types) + if (rawResponse.isSuccessful()) { + response = rawResponse.body(); } else { - messages.add( - new CommitValidationMessage( - "The author does not have a current Eclipse Contributor Agreement (ECA) on file.\n" - + "If there are multiple commits, please ensure that each author has a ECA.", - true)); + // auto close the response resources after fetching + try (ResponseBody err = futureResponse.get().errorBody(); + BufferedSource src = err.source()) { + response = this.responseAdapter.fromJson(src); + } catch (JsonEncodingException e) { + log.error(e.getMessage(), e); + throw new CommitValidationException("An error happened while retrieving validation response, please contact the administrator if this error persists", e); + } + } + for (CommitStatus c : response.commits().values()) { + messages.addAll( + c.messages() + .stream() + .map( + message -> + new CommitValidationMessage( + message.message(), message.code() < 0 && response.trackedProject())) + .collect(Collectors.toList())); addEmptyLine(messages); - errors.add("An Eclipse Contributor Agreement is required."); + if (response.errorCount() > 0 && response.trackedProject()) { + errors.addAll( + c.errors().stream().map(CommitStatusMessage::message).collect(Collectors.toList())); + errors.add("An Eclipse Contributor Agreement is required."); + } } + } catch (IOException | ExecutionException e) { + log.error(e.getMessage(), e); + throw new CommitValidationException("An error happened while checking commit", e); + } catch (InterruptedException e) { + log.error(e.getMessage(), e); + Thread.currentThread().interrupt(); + throw new CommitValidationException("Verification of commit has been interrupted", e); } // TODO Extend exception-throwing delegation to include all possible messages. @@ -150,42 +166,45 @@ public class EclipseCommitValidationListener implements CommitValidationListener } messages.add(new CommitValidationMessage("This commit passes Eclipse validation.", false)); - return messages; } - private boolean isABot(PersonIdent authorIdent, Optional<IdentifiedUser> author) throws CommitValidationException { - try { - if (author.isPresent()) { - Response<List<Bot>> bots = this.apiService.bots(author.get().getUserName().get()).get(); - if (bots.isSuccessful()) return bots.body().stream().anyMatch(b -> b.email() != null && b.email().equals(authorIdent.getEmailAddress())); - } - - // Start a request for all emails, if any match, considered the user a bot - Set<String> emailAddresses = new HashSet<>(); - emailAddresses.add(authorIdent.getEmailAddress()); - - // add all Gerrit email addresses if present - author.ifPresent(u -> emailAddresses.addAll(u.getEmailAddresses())); - List<CompletableFuture<Response<List<Bot>>>> searches = - emailAddresses.stream() - .map(email -> this.apiService.bots(email)) - .collect(Collectors.toList()); - - return anyMatch( - searches, e -> e.isSuccessful() && e.body().stream().anyMatch(a -> a.email() != null && a.email().equals(authorIdent.getEmailAddress()))) - .get() - .booleanValue(); - } catch (ExecutionException e) { - log.error(e.getMessage(), e); - throw new CommitValidationException( - "An error happened while checking if user is a registered bot", e); - } catch (InterruptedException e) { - log.error(e.getMessage(), e); - Thread.currentThread().interrupt(); - throw new CommitValidationException( - "Verification whether user is a registered bot has been interrupted", e); + /** + * Creates request representation of the commit, containing information about the current commit + * and the users associated with it. + * + * @param src the commit associated with this request + * @param author the author of the commit + * @param committer the committer for this request + * @return a Commit object to be posted to the ECA validation service. + */ + private static Commit getRequestCommit(RevCommit src, PersonIdent author, PersonIdent committer) { + // load commit object with information contained in the commit + Commit.Builder c = Commit.builder(); + c.subject(src.getShortMessage()); + c.hash(src.name()); + c.body(src.getFullMessage()); + c.head(true); + + // get the parent commits, and retrieve their hashes + RevCommit[] parents = src.getParents(); + List<String> parentHashes = new ArrayList<>(parents.length); + for (RevCommit parent : parents) { + parentHashes.add(parent.name()); } + c.parents(parentHashes); + + // convert the commit users to objects to be passed to ECA service + GitUser.Builder authorGit = GitUser.builder(); + authorGit.mail(author.getEmailAddress()); + authorGit.name(author.getName()); + GitUser.Builder committerGit = GitUser.builder(); + committerGit.mail(committer.getEmailAddress()); + committerGit.name(committer.getName()); + + c.author(authorGit.build()); + c.committer(committerGit.build()); + return c.build(); } private static void addSeparatorLine(List<CommitValidationMessage> messages) { @@ -199,153 +218,4 @@ public class EclipseCommitValidationListener implements CommitValidationListener private static void addDocumentationPointerMessage(List<CommitValidationMessage> messages) { messages.add(new CommitValidationMessage(ECA_DOCUMENTATION, false)); } - - /** - * Answers whether or not a user has a current committer agreement on file. This determination is - * made based on group membership. Answers <code>true</code> if the user is a member of the - * designated "ECA" group, or <code>false</code> otherwise. - * - * @param userIdent Object representation of user credentials of a Git commit. - * @param user a Gerrit user if present. - * @return <code>true</code> if the user has a current agreement, or <code>false</code> otherwise. - * @throws CommitValidationException - * @throws IOException - */ - private boolean hasCurrentAgreement(PersonIdent userIdent, Optional<IdentifiedUser> user) - throws CommitValidationException { - if (hasCurrentAgreementOnServer(userIdent, user)) { - if (user.isPresent()) { - log.info( - "User with Gerrit accound ID '" - + user.get().getAccountId().get() - + "' is considered having an agreement by " - + APIService.BASE_URL); - } else { - log.info( - "User with Git email address '" - + userIdent.getEmailAddress() - + "' is considered having an agreement by " - + APIService.BASE_URL); - } - - return true; - } else { - if (user.isPresent()) { - log.info( - "User with Gerrit accound ID '" - + user.get().getAccountId().get() - + "' is not considered having an agreement by " - + APIService.BASE_URL); - } else { - log.info( - "User with Git email address '" - + userIdent.getEmailAddress() - + "' is not considered having an agreement by " - + APIService.BASE_URL); - } - } - - if (user.isPresent()) { - log.info( - "User with Gerrit accound ID '" - + user.get().getAccountId().get() - + "' is *not* considered having any agreement"); - } else { - log.info( - "User with Git email address '" - + userIdent.getEmailAddress() - + "' is *not* considered having any agreement"); - } - return false; - } - - private boolean hasCurrentAgreementOnServer( - PersonIdent authorIdent, Optional<IdentifiedUser> user) throws CommitValidationException { - try { - if (user.isPresent()) { - Response<ECA> eca = this.apiService.eca(user.get().getUserName().get()).get(); - if (eca.isSuccessful()) return eca.body().signed(); - } - - // Start a request for all emails, if any match, considered the user having an agreement - Set<String> emailAddresses = new HashSet<>(); - emailAddresses.add(authorIdent.getEmailAddress()); - - // add all Gerrit email addresses if present - user.ifPresent(u -> emailAddresses.addAll(u.getEmailAddresses())); - List<CompletableFuture<Response<List<UserAccount>>>> searches = - emailAddresses.stream() - .map(email -> this.apiService.search(null, null, email)) - .collect(Collectors.toList()); - - return anyMatch( - searches, e -> e.isSuccessful() && e.body().stream().anyMatch(a -> a.eca().signed())) - .get() - .booleanValue(); - } catch (ExecutionException e) { - log.error(e.getMessage(), e); - throw new CommitValidationException( - "An error happened while checking if user has a signed agreement", e); - } catch (InterruptedException e) { - log.error(e.getMessage(), e); - Thread.currentThread().interrupt(); - throw new CommitValidationException( - "Verification whether user has a signed agreement has been interrupted", e); - } - } - - static <T> CompletableFuture<Boolean> anyMatch( - List<? extends CompletionStage<? extends T>> l, Predicate<? super T> criteria) { - CompletableFuture<Boolean> result = new CompletableFuture<>(); - Consumer<T> whenMatching = - v -> { - if (criteria.test(v)) result.complete(Boolean.TRUE); - }; - CompletableFuture.allOf( - l.stream().map(f -> f.thenAccept(whenMatching)).toArray(CompletableFuture<?>[]::new)) - .whenComplete( - (ignored, t) -> { - if (t != null) result.completeExceptionally(t); - else result.complete(Boolean.FALSE); - }); - - return result; - } - - /** - * Answers the Gerrit identity (instance of IdentifiedUser) associated with the author - * credentials, or <code>Optional.empty()</code> if the user cannot be matched to a Gerrit user - * identity. - * - * @param author Object representation of user credentials of a Git commit. - * @return an instance of IdentifiedUser or <code>null</code> if the user cannot be identified by - * Gerrit. - */ - private Optional<IdentifiedUser> identifyUser(PersonIdent author) { - try { - /* - * The gerrit: scheme is, according to documentation on AccountExternalId, - * used for LDAP, HTTP, HTTP_LDAP, and LDAP_BIND usernames (that documentation - * also acknowledges that the choice of name was suboptimal. - * - * We look up both using mailto: and gerrit: - */ - Optional<ExternalId> id = - externalIds.get( - ExternalId.Key.create(ExternalId.SCHEME_MAILTO, author.getEmailAddress())); - if (!id.isPresent()) { - id = - externalIds.get( - ExternalId.Key.create( - ExternalId.SCHEME_GERRIT, author.getEmailAddress().toLowerCase())); - if (!id.isPresent()) { - return Optional.empty(); - } - } - return Optional.of(factory.create(id.get().accountId())); - } catch (ConfigInvalidException | IOException e) { - log.error("Cannot retrieve external id", e); - return Optional.empty(); - } - } } diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/GitUser.java b/src/main/java/org/eclipse/foundation/gerrit/validation/GitUser.java new file mode 100644 index 0000000000000000000000000000000000000000..acf3afdd14b4e5cd8c3e3455540fc31f412e5634 --- /dev/null +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/GitUser.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2020 Eclipse Foundation + * + * <p>This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * <p>SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.foundation.gerrit.validation; + +import com.google.auto.value.AutoValue; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +/** + * Basic object representing a Git users data required for verification written with AutoValue. + * + * @author Martin Lowe + */ +@AutoValue +public abstract class GitUser { + public abstract String name(); + + public abstract String mail(); + + public static JsonAdapter<GitUser> jsonAdapter(Moshi moshi) { + return new AutoValue_GitUser.MoshiJsonAdapter(moshi); + } + + static Builder builder() { + return new AutoValue_GitUser.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder name(String name); + + abstract Builder mail(String mail); + + abstract GitUser build(); + } +} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/OAuthAuthenticator.java b/src/main/java/org/eclipse/foundation/gerrit/validation/OAuthAuthenticator.java deleted file mode 100644 index cd7ece1fa2f20e6c859060d105d17be354e54b5c..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/OAuthAuthenticator.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * ******************************************************************* Copyright (c) 2019 Eclipse - * Foundation and others. - * - * <p>This program and the accompanying materials are made available under the terms of the Eclipse - * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * <p>SPDX-License-Identifier: EPL-2.0 - * ******************************************************************** - */ -package org.eclipse.foundation.gerrit.validation; - -import java.io.IOException; -import java.util.Optional; -import okhttp3.Authenticator; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.Route; - -final class OAuthAuthenticator implements Authenticator { - - static final String AUTHORIZATION = "Authorization"; - - private final AccessTokenProvider accessTokenProvider; - - OAuthAuthenticator(AccessTokenProvider accessTokenProvider) { - this.accessTokenProvider = accessTokenProvider; - } - - @Override - public Request authenticate(Route route, Response response) throws IOException { - synchronized (this) { - Optional<AccessToken> token = this.accessTokenProvider.token(); - Request request = response.request(); - if (request.header(AUTHORIZATION) != null) { - System.err.println("we've already attempted to authenticate"); - // we've already attempted to authenticate - if (responseCount(response) >= 3) { - System.err.println("we don't retry more than 3 times"); - // we don't retry more than 3 times - return null; - } - if (token.isPresent() - && token.get().bearerCredentials().equals(request.header(AUTHORIZATION))) { - // If we already failed with these credentials, try refresh the token - // will return null if refreshToken returns empty - System.err.println( - "failed with current credentials (" + token.get() + "), try refresh the token"); - return updateAuthorizationHeader(request, this.accessTokenProvider.refreshToken()); - } else { - // we failed with different token, let's try with current one. - System.err.println("we failed with different token, let's try with current one."); - return updateAuthorizationHeader(request, token); - } - } else { - // first authentication try, send token - System.err.println("first authentication try"); - if (!token.isPresent()) { - token = this.accessTokenProvider.refreshToken(); - } - return updateAuthorizationHeader(request, token); - } - } - } - - private static Request updateAuthorizationHeader( - Request request, Optional<AccessToken> newToken) { - if (newToken.isPresent()) { - return request.newBuilder().header(AUTHORIZATION, newToken.get().bearerCredentials()).build(); - } else { - return null; - } - } - - private static int responseCount(Response response) { - int result = 1; - while ((response = response.priorResponse()) != null) { - result++; - } - return result; - } -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/RetrofitFactory.java b/src/main/java/org/eclipse/foundation/gerrit/validation/RetrofitFactory.java index 0d414e0bde003bd120f62bc7dccd23474d78446f..7fb5f37e36947d1917b9e8178c8628447d78ad38 100644 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/RetrofitFactory.java +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/RetrofitFactory.java @@ -10,13 +10,20 @@ */ package org.eclipse.foundation.gerrit.validation; -import com.squareup.moshi.Moshi; import java.time.Duration; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + import okhttp3.ConnectionSpec; import okhttp3.Dispatcher; import okhttp3.HttpUrl; @@ -24,19 +31,21 @@ import okhttp3.OkHttpClient; import okhttp3.internal.Util; import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor.Level; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import retrofit2.Retrofit; import retrofit2.converter.moshi.MoshiConverterFactory; final class RetrofitFactory { private static final Logger log = LoggerFactory.getLogger(RetrofitFactory.class); + + static final String AUTHORIZATION = "Authorization"; + private final OkHttpClient client; private final MoshiConverterFactory moshiConverterFactory; + private final Moshi moshi; - RetrofitFactory(String grantType, String clientId, String clientSecret, String scope) { - Moshi moshi = new Moshi.Builder().add(JsonAdapterFactory.create()).build(); - this.moshiConverterFactory = MoshiConverterFactory.create(moshi); + RetrofitFactory() { + this.moshi = new Moshi.Builder().add(JsonAdapterFactory.create()).build(); + this.moshiConverterFactory = MoshiConverterFactory.create(this.moshi); HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor( @@ -47,9 +56,9 @@ final class RetrofitFactory { } }) .setLevel(Level.BASIC); - loggingInterceptor.redactHeader(OAuthAuthenticator.AUTHORIZATION); + loggingInterceptor.redactHeader(AUTHORIZATION); - OkHttpClient baseClient = + this.client = new OkHttpClient.Builder() .callTimeout(Duration.ofSeconds(5)) .dispatcher( @@ -66,25 +75,30 @@ final class RetrofitFactory { // TLS_1_0) .connectionSpecs(Arrays.asList(ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)) .build(); - AccountsService accountsService = - newRetrofit(AccountsService.BASE_URL, baseClient).create(AccountsService.class); - AccessTokenProvider accessTokenProvider = - new AccessTokenProvider(accountsService, grantType, clientId, clientSecret, scope); - - this.client = - baseClient.newBuilder().authenticator(new OAuthAuthenticator(accessTokenProvider)).build(); } - private Retrofit newRetrofit(HttpUrl baseUrl, OkHttpClient client) { + private Retrofit newRetrofit(HttpUrl baseUrl) { return new Retrofit.Builder() .baseUrl(baseUrl) .callbackExecutor(Executors.newSingleThreadExecutor()) .addConverterFactory(this.moshiConverterFactory) - .client(client) + .client(this.client) .build(); } public <T> T newService(HttpUrl baseUrl, Class<T> serviceClass) { - return newRetrofit(baseUrl, this.client).create(serviceClass); + return newRetrofit(baseUrl).create(serviceClass); + } + + /** + * Helper when handling requests, returns an adapter if it is registered within the current Moshi + * object. + * + * @param <T> the type of object to retrieve a JSON adapter for + * @param type the raw class type to retrieve a JSON adapter for + * @return optional with adapter if present + */ + public <T> Optional<JsonAdapter<T>> adapter(Class<T> type) { + return Optional.ofNullable(this.moshi.adapter(type)); } } diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/UserAccount.java b/src/main/java/org/eclipse/foundation/gerrit/validation/UserAccount.java deleted file mode 100644 index cb7f6a9b2e502b06cbd5fc77693ce9ed5ca1ba45..0000000000000000000000000000000000000000 --- a/src/main/java/org/eclipse/foundation/gerrit/validation/UserAccount.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * ******************************************************************* Copyright (c) 2019 Eclipse - * Foundation and others. - * - * <p>This program and the accompanying materials are made available under the terms of the Eclipse - * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * <p>SPDX-License-Identifier: EPL-2.0 - * ******************************************************************** - */ -package org.eclipse.foundation.gerrit.validation; - -import com.google.auto.value.AutoValue; -import com.squareup.moshi.Json; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; -import javax.annotation.Nullable; - -@AutoValue -abstract class UserAccount { - - abstract int uid(); - - abstract String name(); - - @Nullable - abstract String mail(); - - abstract ECA eca(); - - @Json(name = "is_committer") - abstract boolean isCommitter(); - - // TODO: make package-private - public static JsonAdapter<UserAccount> jsonAdapter(Moshi moshi) { - return new AutoValue_UserAccount.MoshiJsonAdapter(moshi).nullSafe(); - } - - static Builder builder() { - return new AutoValue_UserAccount.Builder(); - } - - @AutoValue.Builder - abstract static class Builder { - abstract Builder uid(int uid); - - abstract Builder name(String name); - - abstract Builder mail(String mail); - - abstract Builder eca(ECA eca); - - abstract Builder isCommitter(boolean isCommitter); - - abstract UserAccount build(); - } -} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/ValidationRequest.java b/src/main/java/org/eclipse/foundation/gerrit/validation/ValidationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..b987db92f270d3f2585d1bb8f2348cb56435d160 --- /dev/null +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/ValidationRequest.java @@ -0,0 +1,50 @@ +/** + * ***************************************************************************** Copyright (C) 2020 + * Eclipse Foundation + * + * <p>This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * <p>SPDX-License-Identifier: EPL-2.0 + * **************************************************************************** + */ +package org.eclipse.foundation.gerrit.validation; + +import java.util.List; + +import com.google.auto.value.AutoValue; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +/** + * Represents a request to validate a list of commits. + * + * @author Martin Lowe + */ +@AutoValue +public abstract class ValidationRequest { + public abstract String repoUrl(); + + public abstract List<Commit> commits(); + + public abstract String provider(); + + public static JsonAdapter<ValidationRequest> jsonAdapter(Moshi moshi) { + return new AutoValue_ValidationRequest.MoshiJsonAdapter(moshi); + } + + static Builder builder() { + return new AutoValue_ValidationRequest.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + public abstract Builder repoUrl(String repoUrl); + + public abstract Builder commits(List<Commit> commits); + + public abstract Builder provider(String provider); + + abstract ValidationRequest build(); + } +} diff --git a/src/main/java/org/eclipse/foundation/gerrit/validation/ValidationResponse.java b/src/main/java/org/eclipse/foundation/gerrit/validation/ValidationResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..cc1da3fbbba84be8fae61464d1486d3169acf647 --- /dev/null +++ b/src/main/java/org/eclipse/foundation/gerrit/validation/ValidationResponse.java @@ -0,0 +1,39 @@ +/** + * ***************************************************************************** Copyright (C) 2020 + * Eclipse Foundation + * + * <p>This program and the accompanying materials are made available under the terms of the Eclipse + * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * <p>SPDX-License-Identifier: EPL-2.0 + * **************************************************************************** + */ +package org.eclipse.foundation.gerrit.validation; + +import java.util.Map; + +import com.google.auto.value.AutoValue; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +/** + * Represents an internal response for a call to this API. + * + * @author Martin Lowe + */ +@AutoValue +public abstract class ValidationResponse { + public abstract boolean passed(); + + public abstract int errorCount(); + + public abstract String time(); + + public abstract Map<String, CommitStatus> commits(); + + public abstract boolean trackedProject(); + + public static JsonAdapter<ValidationResponse> jsonAdapter(Moshi moshi) { + return new AutoValue_ValidationResponse.MoshiJsonAdapter(moshi); + } +}