Skip to content
Snippets Groups Projects
Unverified Commit 93bfbd0b authored by Martin Lowe's avatar Martin Lowe :flag_ca: Committed by GitHub
Browse files

Merge pull request #25 from autumnfound/malowe/master/24

Update to make use of the ECA validation service for business logic
parents 6ed1502d b4532366
No related branches found
No related tags found
No related merge requests found
Showing
with 405 additions and 497 deletions
......@@ -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);
}
/**
* ******************************************************************* 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();
}
}
}
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);
}
}
/* 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
/**
* ******************************************************************* Copyright (c) 2019 Eclipse
* Foundation and others.
* ***************************************************************************** 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.Json;
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
abstract class AccessToken {
public abstract class Commit {
public abstract String hash();
@Json(name = "access_token")
abstract String accessToken();
public abstract String subject();
@Json(name = "expires_in")
abstract int expiresInSeconds();
public abstract String body();
@Json(name = "token_type")
abstract String tokenType();
public abstract List<String> parents();
abstract String scope();
public abstract GitUser author();
String bearerCredentials() {
return "Bearer " + accessToken();
}
public abstract GitUser committer();
// TODO: make package-private
public static JsonAdapter<AccessToken> jsonAdapter(Moshi moshi) {
return new AutoValue_AccessToken.MoshiJsonAdapter(moshi);
}
public abstract boolean head();
abstract Builder toBuilder();
public static JsonAdapter<Commit> jsonAdapter(Moshi moshi) {
return new AutoValue_Commit.MoshiJsonAdapter(moshi);
}
static Builder builder() {
return new AutoValue_AccessToken.Builder();
return new AutoValue_Commit.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder accessToken(String accessToken);
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);
abstract Builder expiresInSeconds(int expireInSeconds);
public abstract Builder author(GitUser author);
abstract Builder tokenType(String tokenType);
public abstract Builder committer(GitUser committer);
abstract Builder scope(String scope);
public abstract Builder head(boolean head);
abstract AccessToken build();
abstract Commit build();
}
}
/**
* ***************************************************************************** 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);
}
}
}
......@@ -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 &quot;ECA&quot; 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();
}
}
}
/**
* ******************************************************************* Copyright (c) 2019 Eclipse
* Foundation and others.
* 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.Json;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import javax.annotation.Nullable;
/**
* Basic object representing a Git users data required for verification written with AutoValue.
*
* @author Martin Lowe
*/
@AutoValue
abstract class UserAccount {
abstract int uid();
abstract String name();
public abstract class GitUser {
public abstract String name();
@Nullable
abstract String mail();
public 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();
public static JsonAdapter<GitUser> jsonAdapter(Moshi moshi) {
return new AutoValue_GitUser.MoshiJsonAdapter(moshi);
}
static Builder builder() {
return new AutoValue_UserAccount.Builder();
return new AutoValue_GitUser.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();
abstract GitUser build();
}
}
/**
* ******************************************************************* 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;
}
}
......@@ -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));
}
}
/**
* ******************************************************************* Copyright (c) 2019 Eclipse
* Foundation and others.
* ***************************************************************************** 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.Json;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
/**
* Represents a request to validate a list of commits.
*
* @author Martin Lowe
*/
@AutoValue
abstract class ECA {
public abstract class ValidationRequest {
public abstract String repoUrl();
abstract boolean signed();
public abstract List<Commit> commits();
@Json(name = "can_contribute_spec_project")
abstract boolean canContributeToSpecProject();
public abstract String provider();
// TODO: make package-private
public static JsonAdapter<ECA> jsonAdapter(Moshi moshi) {
return new AutoValue_ECA.MoshiJsonAdapter(moshi);
public static JsonAdapter<ValidationRequest> jsonAdapter(Moshi moshi) {
return new AutoValue_ValidationRequest.MoshiJsonAdapter(moshi);
}
static Builder builder() {
return new AutoValue_ECA.Builder();
return new AutoValue_ValidationRequest.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder signed(boolean signed);
public abstract Builder repoUrl(String repoUrl);
public abstract Builder commits(List<Commit> commits);
abstract Builder canContributeToSpecProject(boolean canContributeToSpecProject);
public abstract Builder provider(String provider);
abstract ECA build();
abstract ValidationRequest build();
}
}
/**
* ******************************************************************* Copyright (c) 2019 Eclipse
* Foundation and others.
* ***************************************************************************** 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.concurrent.CompletableFuture;
import okhttp3.HttpUrl;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
import java.util.Map;
interface AccountsService {
import com.google.auto.value.AutoValue;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
static final HttpUrl BASE_URL = HttpUrl.get("https://accounts.eclipse.org/");
/**
* 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();
@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);
public static JsonAdapter<ValidationResponse> jsonAdapter(Moshi moshi) {
return new AutoValue_ValidationResponse.MoshiJsonAdapter(moshi);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment