Commit ab2a09da authored by Martin Lowe's avatar Martin Lowe 🇨🇦 Committed by Christopher Guindon
Browse files

Added support for forks and untracked projects in API and pre-recieve



Added logic to retrieve and check for information regarding parent
project if fork. Added support in API + ECA script for untracked/managed
projects.
Signed-off-by: Martin Lowe's avatarMartin Lowe <martin.lowe@eclipse-foundation.org>
parent ee681e3f
......@@ -124,4 +124,26 @@ public class Commit {
public void setHead(boolean head) {
this.head = head;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Commit [hash=");
builder.append(hash);
builder.append(", subject=");
builder.append(subject);
builder.append(", body=");
builder.append(body);
builder.append(", parents=");
builder.append(parents);
builder.append(", author=");
builder.append(author);
builder.append(", committer=");
builder.append(committer);
builder.append(", head=");
builder.append(head);
builder.append("]");
return builder.toString();
}
}
......@@ -23,10 +23,12 @@ import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
*/
public class CommitStatus {
private List<CommitStatusMessage> messages;
private List<CommitStatusMessage> warnings;
private List<CommitStatusMessage> errors;
public CommitStatus() {
this.messages = new ArrayList<>();
this.warnings = new ArrayList<>();
this.errors = new ArrayList<>();
}
......@@ -52,6 +54,28 @@ public class CommitStatus {
this.messages.add(new CommitStatusMessage(code, message));
}
/**
* @return the warnings
*/
public List<CommitStatusMessage> getWarnings() {
return new ArrayList<>(warnings);
}
/**
* @param warnings the warnings to set
*/
public void setWarnings(List<CommitStatusMessage> warnings) {
this.warnings = new ArrayList<>(warnings);
}
/**
* @param warning warning to add to current commit status
* @param code the status code for the message
*/
public void addWarning(String warning, APIStatusCode code) {
this.warnings.add(new CommitStatusMessage(code, warning));
}
/**
* @return the errs
*/
......
......@@ -9,6 +9,7 @@
******************************************************************************/
package org.eclipsefoundation.git.eca.model;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
......@@ -21,21 +22,21 @@ import org.eclipsefoundation.git.eca.namespace.ProviderType;
*
*/
public class ValidationRequest {
private String repoUrl;
private URI repoUrl;
private List<Commit> commits;
private ProviderType provider;
/**
* @return the repoUrl
*/
public String getRepoUrl() {
public URI getRepoUrl() {
return repoUrl;
}
/**
* @param repoUrl the repoUrl to set
*/
public void setRepoUrl(String repoUrl) {
public void setRepoUrl(URI repoUrl) {
this.repoUrl = repoUrl;
}
......@@ -66,4 +67,18 @@ public class ValidationRequest {
public void setProvider(ProviderType provider) {
this.provider = provider;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ValidationRequest [repoUrl=");
builder.append(repoUrl);
builder.append(", commits=");
builder.append(commits);
builder.append(", provider=");
builder.append(provider);
builder.append("]");
return builder.toString();
}
}
......@@ -29,6 +29,7 @@ public class ValidationResponse {
private int errorCount;
private Date time;
private Map<String, CommitStatus> commits;
private boolean trackedProject;
public ValidationResponse() {
this.commits = new HashMap<>();
......@@ -99,6 +100,13 @@ public class ValidationResponse {
commits.computeIfAbsent(getHashKey(hash), k -> new CommitStatus()).addMessage(message, code);
}
/**
* @param warning message to add to the API response
*/
public void addWarning(String hash, String warning, APIStatusCode code) {
commits.computeIfAbsent(getHashKey(hash), k -> new CommitStatus()).addWarning(warning, code);
}
/**
* @param error message to add to the API response
*/
......@@ -106,6 +114,20 @@ public class ValidationResponse {
commits.computeIfAbsent(getHashKey(hash), k -> new CommitStatus()).addError(error, code);
}
/**
* @return the trackedProject
*/
public boolean isTrackedProject() {
return trackedProject;
}
/**
* @param trackedProject the trackedProject to set
*/
public void setTrackedProject(boolean trackedProject) {
this.trackedProject = trackedProject;
}
private String getHashKey(String hash) {
return hash == null ? "_nil" : hash;
}
......
......@@ -9,6 +9,7 @@
******************************************************************************/
package org.eclipsefoundation.git.eca.resource;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
......@@ -72,7 +73,7 @@ public class ValidationResource {
CachingService cache;
@Inject
ProjectsService projects;
/**
* Consuming a JSON request, this method will validate all passed commits, using
* the repo URL and the repository provider. These commits will be validated to
......@@ -84,6 +85,7 @@ public class ValidationResource {
* @return a web response indicating success or failure for each commit, along
* with standard messages that may be used to give users context on
* failure.
* @throws MalformedURLException
*/
@POST
public Response validate(ValidationRequest req) {
......@@ -100,12 +102,17 @@ public class ValidationResource {
if (req.getProvider() == null) {
addError(r, "A provider needs to be set to validate a request", null);
}
// only process if we have no errors
if (r.getErrorCount() == 0) {
LOGGER.debug("Processing: {}", req);
// filter the projects based on the repo URL. At least one repo in project must
// match the repo URL to be valid
List<Project> filteredProjects = retrieveProjectsForRequest(req);
// set whether this call has tracked projects
r.setTrackedProject(!filteredProjects.isEmpty());
for (Commit c : req.getCommits()) {
// process the request, capturing if we should continue processing
boolean continueProcessing = processCommit(c, r, req);
boolean continueProcessing = processCommit(c, r, filteredProjects);
// if there is a reason to stop processing, break the loop
if (!continueProcessing) {
break;
......@@ -124,12 +131,12 @@ public class ValidationResource {
* author and committers Eclipse Account is retrieved, which are then used to
* check if the current commit is valid for the current project.
*
* @param c the commit to process
* @param response the response container
* @param request the current validation request
* @param c the commit to process
* @param response the response container
* @param filteredProjects tracked projects for the current request
* @return true if we should continue processing, false otherwise.
*/
private boolean processCommit(Commit c, ValidationResponse response, ValidationRequest request) {
private boolean processCommit(Commit c, ValidationResponse response, List<Project> filteredProjects) {
// ensure the commit is valid, and has required fields
if (!CommitHelper.validateCommit(c)) {
addError(response, "One or more commits were invalid. Please check the payload and try again", c.getHash());
......@@ -173,10 +180,11 @@ public class ValidationResource {
return true;
}
// validate author access to the current repo
validateAuthorAccess(response, c, eclipseAuthor, request);
validateAuthorAccess(response, c, eclipseAuthor, filteredProjects);
// only committers can push on behalf of other users
if (!eclipseAuthor.equals(eclipseCommitter) && !isCommitter(response, eclipseCommitter, c.getHash(), request)) {
if (!eclipseAuthor.equals(eclipseCommitter)
&& !isCommitter(response, eclipseCommitter, c.getHash(), filteredProjects)) {
addMessage(response, "You are not a project committer.", c.getHash());
addMessage(response, "Only project committers can push on behalf of others.", c.getHash());
addError(response, "You must be a committer to push on behalf of others.", c.getHash());
......@@ -189,17 +197,15 @@ public class ValidationResource {
* recorded in the response for the current request to be returned once all
* validation checks are completed.
*
* @param r the current response object for the request
* @param c the commit that is being validated
* @param eclipseAuthor the user to validate on a branch
* @param repoUrl repo URL for the current commit set, to be used when
* checking projects
* @param r the current response object for the request
* @param c the commit that is being validated
* @param eclipseAuthor the user to validate on a branch
* @param filteredProjects tracked projects for the current request
*/
private void validateAuthorAccess(ValidationResponse r, Commit c, EclipseUser eclipseAuthor,
ValidationRequest req) {
List<Project> filteredProjects) {
// check if the author matches to an eclipse user and is a committer
if (isCommitter(r, eclipseAuthor, c.getHash(), req)) {
if (isCommitter(r, eclipseAuthor, c.getHash(), filteredProjects)) {
addMessage(r, "The author is a committer on the project.", c.getHash());
} else {
addMessage(r, "The author is not a committer on the project.", c.getHash());
......@@ -212,6 +218,7 @@ public class ValidationResource {
+ "If there are multiple Typecommits, please ensure that each author has a ECA.",
c.getHash());
addError(r, "An Eclipse Contributor Agreement is required.", c.getHash());
}
// retrieve the email of the Signed-off-by footer
......@@ -225,6 +232,7 @@ public class ValidationResource {
c.getHash());
addError(r, "The contributor must \"sign-off\" on the contribution.", c.getHash(),
APIStatusCode.ERROR_SIGN_OFF);
}
}
}
......@@ -238,18 +246,13 @@ public class ValidationResource {
* the given project. If they match for the given project, they are granted
* committer-like access to the repository.
*
* @param r the current response object for the request
* @param user the user to validate on a branch
* @param hash the hash of the commit that is being validated
* @param repoUrl repo URL for the current commit set, to be used when checking
* projects
* @param r the current response object for the request
* @param user the user to validate on a branch
* @param hash the hash of the commit that is being validated
* @param filteredProjects tracked projects for the current request
* @return true if user is considered a committer, false otherwise.
*/
private boolean isCommitter(ValidationResponse r, EclipseUser user, String hash, ValidationRequest req) {
// filter the projects based on the repo URL. At least one repo in project must
// match the repo URL to be valid
List<Project> filteredProjects = retrieveProjectsForRequest(req);
private boolean isCommitter(ValidationResponse r, EclipseUser user, String hash, List<Project> filteredProjects) {
// iterate over filtered projects
for (Project p : filteredProjects) {
LOGGER.debug("Checking project '{}' for user '{}'", p.getName(), user.getName());
......@@ -301,27 +304,27 @@ public class ValidationResource {
* if none found.
*/
private List<Project> retrieveProjectsForRequest(ValidationRequest req) {
String repoUrl = req.getRepoUrl();
String repoUrl = req.getRepoUrl().getPath();
// check for all projects that make use of the given repo
List<Project> availableProjects = projects.getProjects();
if (availableProjects == null || availableProjects.isEmpty()) {
return Collections.emptyList();
}
LOGGER.debug("Number of projects found: {}", availableProjects.size());
LOGGER.debug("Checking projects for repos that end with: {}", repoUrl);
// filter the projects based on the repo URL. At least one repo in project must
// match the repo URL to be valid
if (ProviderType.GITLAB.equals(req.getProvider())) {
return availableProjects.stream()
.filter(p -> p.getGitlabRepos().stream().anyMatch(re -> re.getUrl().equals(repoUrl)))
.filter(p -> p.getGitlabRepos().stream().anyMatch(re -> re.getUrl().endsWith(repoUrl)))
.collect(Collectors.toList());
} else if (ProviderType.GITHUB.equals(req.getProvider())) {
return availableProjects.stream()
.filter(p -> p.getGithubRepos().stream().anyMatch(re -> re.getUrl().equals(repoUrl)))
.filter(p -> p.getGithubRepos().stream().anyMatch(re -> re.getUrl().endsWith(repoUrl)))
.collect(Collectors.toList());
} else {
return availableProjects.stream()
.filter(p -> p.getRepos().stream().anyMatch(re -> re.getUrl().equals(repoUrl)))
.filter(p -> p.getRepos().stream().anyMatch(re -> re.getUrl().endsWith(repoUrl)))
.collect(Collectors.toList());
}
}
......@@ -377,6 +380,11 @@ public class ValidationResource {
private void addError(ValidationResponse r, String message, String hash, APIStatusCode code) {
LOGGER.error(message);
r.addError(hash, message, code);
// only add as strict error for tracked projects
if (r.isTrackedProject()) {
r.addError(hash, message, code);
} else {
r.addWarning(hash, message, code);
}
}
}
......@@ -4,6 +4,19 @@ require 'json'
require 'httparty'
require 'multi_json'
## read in the access token from secret file
if (!File.file?("/etc/gitlab/eca-access-token"))
puts "GL-HOOK-ERR: Internal server error, please contact administrator. Error, secret not found"
exit 1
end
access_token_file = File.open("/etc/gitlab/eca-access-token")
access_token = access_token_file.read.chomp
access_token_file.close
if (access_token.empty?)
puts "GL-HOOK-ERR: Internal server error, please contact administrator. Error, secret not found"
exit 1
end
## Read in the arguments passed from GitLab and split them to an arg array
stdin_raw = ARGF.read;
stdin_args = stdin_raw.split(/\s+/)
......@@ -41,11 +54,18 @@ end
## Get the project ID from env var, extracting from pattern 'project-###'
project_id = ENV['GL_REPOSITORY'][8..-1]
## Get data about project from API
project_response = HTTParty.get("https://gitlab.eclipse.org/api/v4/projects/#{project_id}")
project_response = HTTParty.get("https://gitlab.eclipse.org/api/v4/projects/#{project_id}",
:headers => {
'Authorization' => 'Bearer ' + access_token
})
## Format data to be able to easily read and process it
project_json_data = MultiJson.load(project_response.body)
## Get the web URL
project_url = project_json_data['web_url']
## Get the web URL, checking if project is a fork to get original project URL
if (!project_json_data['forked_from_project'].nil? && !project_json_data['forked_from_project']['web_url'].nil?)
project_url = project_json_data['forked_from_project']['web_url']
else
project_url = project_json_data['web_url']
end
## Create the JSON payload
json_data = {
......@@ -54,7 +74,10 @@ json_data = {
:commits => processed_git_data
}
## Generate request
response = HTTParty.post("https://api.eclipse.org/git/eca", :body => MultiJson.dump(json_data), :headers => { 'Content-Type' => 'application/json' })
response = HTTParty.post("https://api.eclipse.org/git/eca", :body => MultiJson.dump(json_data),
:headers => {
'Content-Type' => 'application/json'
})
## convert request to hash map
parsed_response = MultiJson.load(response.body)
......@@ -67,6 +90,12 @@ commit_keys.each do |key|
commit_status['messages'].each do |msg|
puts "\t#{msg['message']}"
end
if (commit_status['warnings'].empty?) then
commit_status['messages'].each do |msg|
puts "\t#{msg['message']}"
end
puts "Any warnings noted above may indicate compliance issues with committer ECA requirements. More information may be found on https://www.eclipse.org/legal/ECA.php"
end
puts "\n\n"
else
puts "Commit: #{key}\t\tX\n\n"
......
......@@ -12,6 +12,8 @@ package org.eclipsefoundation.git.eca.resource;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
......@@ -36,10 +38,10 @@ import io.restassured.http.ContentType;
*
*/
@QuarkusTest
public class ValidationResourceTest {
class ValidationResourceTest {
@Test
public void validate() {
void validate() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("The Wizard");
......@@ -58,7 +60,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/sample");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -73,7 +75,7 @@ public class ValidationResourceTest {
}
@Test
public void validateMultipleCommits() {
void validateMultipleCommits() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("The Wizard");
......@@ -106,7 +108,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/sample");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -121,7 +123,7 @@ public class ValidationResourceTest {
}
@Test
public void validateMergeCommit() {
void validateMergeCommit() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("Rando Calressian");
......@@ -140,7 +142,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/sample");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
vr.setCommits(commits);
// test output w/ assertions
// No errors expected, should pass as only commit is a valid merge commit
......@@ -155,7 +157,7 @@ public class ValidationResourceTest {
}
@Test
public void validateCommitNoSignOffCommitter() {
void validateCommitNoSignOffCommitter() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("Grunts McGee");
......@@ -174,7 +176,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/prototype");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -190,7 +192,7 @@ public class ValidationResourceTest {
}
@Test
public void validateCommitNoSignOffNonCommitter() {
void validateCommitNoSignOffNonCommitter() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("The Wizard");
......@@ -209,7 +211,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/prototype");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -227,7 +229,7 @@ public class ValidationResourceTest {
}
@Test
public void validateCommitInvalidSignOff() {
void validateCommitInvalidSignOff() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("Barshall Blathers");
......@@ -246,7 +248,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/prototype");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -264,7 +266,7 @@ public class ValidationResourceTest {
}
@Test
public void validateWorkingGroupSpecAccess() {
void validateWorkingGroupSpecAccess() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("The Wizard");
......@@ -288,7 +290,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/tck-proto");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -316,7 +318,7 @@ public class ValidationResourceTest {
vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/tck-proto");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -334,7 +336,7 @@ public class ValidationResourceTest {
}
@Test
public void validateProxyCommitPush() {
void validateProxyCommitPush() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("The Wizard");
......@@ -358,7 +360,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/tck-proto");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -386,7 +388,7 @@ public class ValidationResourceTest {
vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/prototype");
vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
vr.setCommits(commits);
// test output w/ assertions
......@@ -402,7 +404,7 @@ public class ValidationResourceTest {
}
@Test
public void validateNoECA() {
void validateNoECA() throws URISyntaxException {
// set up test users
GitUser g1 = new GitUser();
g1.setName("Newbie Anon");
......@@ -421,7 +423,7 @@ public class ValidationResourceTest {
ValidationRequest vr = new ValidationRequest();
vr.setProvider(ProviderType.GITHUB);
vr.setRepoUrl("http://www.github.com/eclipsefdn/sample");