From 7ea8345afd9360f99608af5bd0b4716a1302dc1d Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Wed, 16 Nov 2022 12:04:38 -0500 Subject: [PATCH 01/10] Iss #98 - Initial commit of Github webhooks code Additionally contains code to move API client models to a separate package to allow for application specific models to live separately from consumed upstream models. --- .gitignore | 1 + pom.xml | 4 + .../git/eca/api/GithubAPI.java | 46 +++++ .../git/eca/api/models/GithubAccessToken.java | 43 ++++ .../git/eca/api/models/GithubCommit.java | 79 ++++++++ .../api/models/GithubCommitStatusRequest.java | 44 +++++ .../eca/api/models/GithubWebhookRequest.java | 136 +++++++++++++ .../models}/InterestGroupData.java | 6 +- .../eca/{model => api/models}/Project.java | 2 +- .../eca/{model => api/models}/SystemHook.java | 2 +- .../git/eca/helper/CommitHelper.java | 2 +- .../eca/namespace/GithubCommitStatuses.java | 39 ++++ .../eca/resource/GithubWebhooksResource.java | 183 ++++++++++++++++++ .../git/eca/resource/ValidationResource.java | 7 +- .../git/eca/resource/WebhooksResource.java | 35 +--- .../git/eca/service/InterestGroupService.java | 4 +- .../git/eca/service/ProjectsService.java | 2 +- .../git/eca/service/SystemHookService.java | 2 +- .../git/eca/service/UserService.java | 4 +- .../git/eca/service/ValidationService.java | 2 +- .../eca/service/impl/CachedUserService.java | 2 +- .../impl/DefaultInterestGroupService.java | 4 +- .../impl/DefaultSystemHookService.java | 2 +- .../impl/PaginationProjectsService.java | 2 +- .../resources/META-INF/resources/favicon.ico | Bin 0 -> 1150 bytes src/main/resources/application.properties | 10 + .../eca/resource/WebhoooksResourceTest.java | 4 +- .../service/impl/CachedUserServiceTest.java | 2 +- .../impl/PaginationProjectsServiceTest.java | 4 +- .../git/eca/test/api/MockProjectsAPI.java | 12 +- 30 files changed, 628 insertions(+), 57 deletions(-) create mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java create mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java create mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommit.java create mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommitStatusRequest.java create mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java rename src/main/java/org/eclipsefoundation/git/eca/{model => api/models}/InterestGroupData.java (92%) rename src/main/java/org/eclipsefoundation/git/eca/{model => api/models}/Project.java (99%) rename src/main/java/org/eclipsefoundation/git/eca/{model => api/models}/SystemHook.java (98%) create mode 100644 src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java create mode 100644 src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java create mode 100644 src/main/resources/META-INF/resources/favicon.ico diff --git a/.gitignore b/.gitignore index 51fe9e44..3b2dca2c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ release.properties secret/ secrets/ secret.properties +*.pem # Additional build resources src/test/resources/schemas diff --git a/pom.xml b/pom.xml index 216db0fa..6708f7d4 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,10 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-context-propagation</artifactId> </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-smallrye-jwt</artifactId> + </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-client</artifactId> diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java new file mode 100644 index 00000000..dfe002f6 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; +import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; + +/** + * @author martin + * + */ +@RegisterRestClient(baseUri = "https://api.github.com") +@Produces("application/json") +public interface GithubAPI { + + @GET + @Path("repos/{repoFull}/pulls/{pullNumber}/commits") + public Response getCommits(@PathParam("repoFull") String repoFull, @PathParam("pullNumber") int pullNumber); + + @POST + @Path("repos/{repoFull}/statuses/{prHeadSha}") + public Response updateStatus(@HeaderParam("Authorization") String bearer, @PathParam("repoFull") String repoFull, + @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate); + + @POST + @Path("app/installations/{installationId}/access_tokens") + public GithubAccessToken getNewAccessToken(@HeaderParam("Authorization") String bearer, @PathParam("installationId") String installationId); +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java new file mode 100644 index 00000000..437e4a6c --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api.models; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +/** + * @author martin + * + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_GithubAccessToken.Builder.class) +public abstract class GithubAccessToken { + + public abstract String getToken(); + public abstract LocalDateTime getExpiresAt(); + + public static Builder builder() { + return new AutoValue_GithubAccessToken.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setToken(String token); + public abstract Builder setExpiresAt(LocalDateTime expiresAt); + + public abstract GithubAccessToken build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommit.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommit.java new file mode 100644 index 00000000..811c9d14 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommit.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api.models; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +/** + * @author Martin Lowe + * + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_GithubCommit.Builder.class) +public abstract class GithubCommit { + public abstract String getSha(); + public abstract CommitData getCommit(); + + public static Builder builder() { + return new AutoValue_GithubCommit.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setSha(String sha); + public abstract Builder setCommit(CommitData commit); + + public abstract GithubCommit build(); + } + + @AutoValue + @JsonDeserialize(builder = AutoValue_GithubCommit_CommitData.Builder.class) + public abstract static class CommitData { + public abstract CommitUser getAuthor(); + public abstract CommitUser getCommitter(); + + public static Builder builder() { + return new AutoValue_GithubCommit_CommitData.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setAuthor(CommitUser author); + public abstract Builder setCommitter(CommitUser committer); + + public abstract CommitData build(); + } + } + @AutoValue + @JsonDeserialize(builder = AutoValue_GithubCommit_CommitUser.Builder.class) + public abstract static class CommitUser { + public abstract String getName(); + public abstract String getEmail(); + + public static Builder builder() { + return new AutoValue_GithubCommit_CommitUser.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setName(String name); + public abstract Builder setEmail(String email); + + public abstract CommitUser build(); + } + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommitStatusRequest.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommitStatusRequest.java new file mode 100644 index 00000000..53522c1f --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubCommitStatusRequest.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api.models; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +/** + * @author martin + * + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_GithubCommitStatusRequest.Builder.class) +public abstract class GithubCommitStatusRequest { + public abstract String getState(); + public abstract String getTargetUrl(); + public abstract String getDescription(); + public abstract String getContext(); + + public static Builder builder() { + return new AutoValue_GithubCommitStatusRequest.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setState(String state); + public abstract Builder setTargetUrl(String targetUrl); + public abstract Builder setDescription(String description); + public abstract Builder setContext(String context); + + public abstract GithubCommitStatusRequest build(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java new file mode 100644 index 00000000..df829081 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.api.models; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.auto.value.AutoValue; + +/** + * Represents incoming webhook requests from the Git ECA app installations on Github. + * + * @author Martin Lowe + * + */ +@AutoValue +@JsonDeserialize(builder = AutoValue_GithubWebhookRequest.Builder.class) +public abstract class GithubWebhookRequest { + + public abstract String getAction(); + + public abstract Installation getInstallation(); + + public abstract Repository getRepository(); + + public abstract PullRequest getPullRequest(); + + public static Builder builder() { + return new AutoValue_GithubWebhookRequest.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setAction(String action); + + public abstract Builder setInstallation(Installation installation); + + public abstract Builder setRepository(Repository repository); + + public abstract Builder setPullRequest(PullRequest pullRequest); + + public abstract GithubWebhookRequest build(); + } + + @AutoValue + @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_Repository.Builder.class) + public abstract static class Repository { + + public abstract String getFullName(); + + public abstract String getHtmlUrl(); + + public static Builder builder() { + return new AutoValue_GithubWebhookRequest_Repository.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setFullName(String fullName); + + public abstract Builder setHtmlUrl(String htmlUrl); + + public abstract Repository build(); + } + } + + @AutoValue + @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_PullRequest.Builder.class) + public abstract static class PullRequest { + + public abstract Integer getNumber(); + public abstract PullRequestHead getHead(); + + public static Builder builder() { + return new AutoValue_GithubWebhookRequest_PullRequest.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setNumber(Integer number); + public abstract Builder setHead(PullRequestHead head); + + public abstract PullRequest build(); + } + } + + @AutoValue + @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_PullRequestHead.Builder.class) + public abstract static class PullRequestHead { + + public abstract String getSha(); + + public static Builder builder() { + return new AutoValue_GithubWebhookRequest_PullRequestHead.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setSha(String sha); + + public abstract PullRequestHead build(); + } + } + + @AutoValue + @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_Installation.Builder.class) + public abstract static class Installation { + public abstract String getId(); + public abstract String getWebhookId(); + + public static Builder builder() { + return new AutoValue_GithubWebhookRequest_Installation.Builder(); + } + + @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "set") + public abstract static class Builder { + public abstract Builder setId(String id); + public abstract Builder setWebhookId(String webhookId); + + public abstract Installation build(); + } + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/InterestGroupData.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java similarity index 92% rename from src/main/java/org/eclipsefoundation/git/eca/model/InterestGroupData.java rename to src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java index e2fd06bd..bea777d5 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/InterestGroupData.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java @@ -1,9 +1,9 @@ -package org.eclipsefoundation.git.eca.model; +package org.eclipsefoundation.git.eca.api.models; import java.util.List; -import org.eclipsefoundation.git.eca.model.Project.GitlabProject; -import org.eclipsefoundation.git.eca.model.Project.User; +import org.eclipsefoundation.git.eca.api.models.Project.GitlabProject; +import org.eclipsefoundation.git.eca.api.models.Project.User; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/Project.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java similarity index 99% rename from src/main/java/org/eclipsefoundation/git/eca/model/Project.java rename to src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java index ba5be582..fa07ce5d 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/Project.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java @@ -9,7 +9,7 @@ * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -package org.eclipsefoundation.git.eca.model; +package org.eclipsefoundation.git.eca.api.models; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/SystemHook.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/SystemHook.java similarity index 98% rename from src/main/java/org/eclipsefoundation/git/eca/model/SystemHook.java rename to src/main/java/org/eclipsefoundation/git/eca/api/models/SystemHook.java index b352d59b..ddc7fd66 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/SystemHook.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/SystemHook.java @@ -9,7 +9,7 @@ * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -package org.eclipsefoundation.git.eca.model; +package org.eclipsefoundation.git.eca.api.models; import java.time.ZonedDateTime; import java.util.List; diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java index 8831591f..0650a2e8 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java +++ b/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java @@ -16,8 +16,8 @@ import java.util.stream.Collectors; import javax.ws.rs.core.MultivaluedMap; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.model.Commit; -import org.eclipsefoundation.git.eca.model.Project; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java new file mode 100644 index 00000000..85f7c758 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.namespace; + +/** + * @author martin + * + */ +public enum GithubCommitStatuses { + + SUCCESS("The author(s) of the pull request is covered by necessary legal agreements in order to proceed!"), + FAILURE("An unexpected error has occurred. Please contact webmaster@eclipse.org."), + ERROR("The author(s) of the pull request is not covered by necessary legal agreements in order to proceed."), + PENDING("Eclipse Foundation Contributor Agreement validation is in progress."); + + private String message; + + private GithubCommitStatuses(String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return this.name().toLowerCase(); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java new file mode 100644 index 00000000..a9245a6f --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.resource; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.eclipsefoundation.core.model.RequestWrapper; +import org.eclipsefoundation.core.service.APIMiddleware; +import org.eclipsefoundation.git.eca.api.GithubAPI; +import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; +import org.eclipsefoundation.git.eca.api.models.GithubCommit; +import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; +import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest; +import org.eclipsefoundation.git.eca.model.Commit; +import org.eclipsefoundation.git.eca.model.GitUser; +import org.eclipsefoundation.git.eca.model.ValidationRequest; +import org.eclipsefoundation.git.eca.namespace.GithubCommitStatuses; +import org.eclipsefoundation.git.eca.namespace.ProviderType; +import org.eclipsefoundation.git.eca.service.ValidationService; +import org.jboss.resteasy.annotations.jaxrs.HeaderParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.quarkus.cache.CacheResult; +import io.smallrye.jwt.algorithm.SignatureAlgorithm; +import io.smallrye.jwt.auth.principal.JWTParser; +import io.smallrye.jwt.auth.principal.ParseException; +import io.smallrye.jwt.build.Jwt; + +/** + * @author martin + * + */ +@Path("webhooks/github") +public class GithubWebhooksResource { + private static final Logger LOGGER = LoggerFactory.getLogger(GithubWebhooksResource.class); + + @ConfigProperty(name = "eclipse.webhooks.github.secret") + String secret; + @ConfigProperty(name = "eclipse.webhooks.github.context") + String context; + + @Inject + RequestWrapper wrapper; + @Inject + APIMiddleware middleware; + @Inject + ValidationService validationService; + + @Inject + JWTParser parser; + + @RestClient + GithubAPI ghApi; + + @GET + @Path("jwt/{installationId}") + public Response test(@PathParam("installationId") String installId) throws ParseException { + + return Response.ok(parser.verify(generateJwt(), secret)).build(); + } + + @POST + @Path("check") + public Response processGithubCheck(@HeaderParam("X-GitHub-Delivery") String deliveryId, @HeaderParam("X-GitHub-Event") String eventType, + GithubWebhookRequest request) { + // If event isn't a pr event, drop as we don't process them + if (!"pull_request".equalsIgnoreCase(eventType)) { + return Response.ok().build(); + } + // start validation process + ValidationRequest vr = generateRequest(request); + String fingerprint = validationService.generateRequestHash(vr); + updateCommitStatus(request, GithubCommitStatuses.PENDING, fingerprint); + + LOGGER.debug("Would have validated: {}", vr); + // TODO once validation move update done, use vr above to validate + boolean hasPassed = true; + if (hasPassed) { + updateCommitStatus(request, GithubCommitStatuses.SUCCESS, fingerprint); + } else { + updateCommitStatus(request, GithubCommitStatuses.FAILURE, fingerprint); + } + return Response.ok().build(); + } + + /** + * Sends off a POST to update the commit status given a context for the current PR. + * + * @param request the current webhook update request + * @param state the state to set the status to + * @param fingerprint + */ + private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state, String fingerprint) { + ghApi + .updateStatus("Bearer " + getAccessToken(request.getInstallation().getId()), request.getRepository().getFullName(), + request.getPullRequest().getHead().getSha(), + GithubCommitStatusRequest + .builder() + .setDescription(state.getMessage()) + .setState(state.toString()) + .setTargetUrl("https://api.eclipse.org/git/eca/status/" + fingerprint + "/ui") + .setContext(context) + .build()); + } + + /** + * Generate the validation request for the current GH Webhook request. + * + * @param request the current webhook request for validation + * @return the Validation Request body to be used in validating data + */ + private ValidationRequest generateRequest(GithubWebhookRequest request) { + // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful + List<GithubCommit> commits = middleware + .getAll(i -> ghApi.getCommits(request.getRepository().getFullName(), request.getPullRequest().getNumber()), + GithubCommit.class); + LOGGER + .trace("Retrieved {} commits for PR {} in repo {}", commits.size(), request.getPullRequest().getNumber(), + request.getRepository().getHtmlUrl()); + // set up the validation request from current data + return ValidationRequest + .builder() + .setProvider(ProviderType.GITHUB) + .setRepoUrl(URI.create(request.getRepository().getHtmlUrl())) + .setCommits(commits + .stream() + .map(c -> Commit + .builder() + .setHash(c.getSha()) + .setAuthor(GitUser + .builder() + .setMail(c.getCommit().getAuthor().getEmail()) + .setName(c.getCommit().getAuthor().getName()) + .build()) + .setCommitter(GitUser + .builder() + .setMail(c.getCommit().getCommitter().getEmail()) + .setName(c.getCommit().getCommitter().getName()) + .build()) + .build()) + .collect(Collectors.toList())) + .build(); + } + + @CacheResult(cacheName = "accesstoken") + protected GithubAccessToken getAccessToken(String installationId) { + return ghApi.getNewAccessToken("Bearer " + generateJwt(), installationId); + } + + /** + * Generate the JWT to use for Github requests. This is stored in a special one-shot cache method secured to this class, + * and not to be used for other requests that require JWTs. + * + * @return signed JWT using the issuer and secret defined in the secret properties. + */ + private String generateJwt() { + return Jwt.subject("EclipseWebmaster").signWithSecret(secret); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java index 6345a0f6..d164c0af 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java @@ -30,9 +30,12 @@ import javax.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.core.service.CachingService; -import org.eclipsefoundation.git.eca.api.models.EclipseUser; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.helper.CommitHelper; +import org.eclipsefoundation.git.eca.model.Commit; +import org.eclipsefoundation.git.eca.model.EclipseUser; +import org.eclipsefoundation.git.eca.model.GitUser; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.namespace.APIStatusCode; diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java index c94fc8bb..a9fd5b26 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/WebhooksResource.java @@ -17,7 +17,7 @@ import javax.ws.rs.Path; import javax.ws.rs.core.Response; import org.eclipsefoundation.core.model.RequestWrapper; -import org.eclipsefoundation.git.eca.model.SystemHook; +import org.eclipsefoundation.git.eca.api.models.SystemHook; import org.eclipsefoundation.git.eca.namespace.EventType; import org.eclipsefoundation.git.eca.service.SystemHookService; import org.jboss.resteasy.annotations.jaxrs.HeaderParam; @@ -27,14 +27,12 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -@Path("/webhooks") +@Path("webhooks/gitlab") public class WebhooksResource { - private static final Logger LOGGER = LoggerFactory.getLogger(WebhooksResource.class); @Inject RequestWrapper wrapper; - @Inject SystemHookService hookService; @@ -42,30 +40,26 @@ public class WebhooksResource { ObjectMapper om; @POST - @Path("/gitlab/system") + @Path("system") public Response processGitlabHook(@HeaderParam("X-Gitlab-Event") String eventHeader, String jsonBody) { - // Do not process if header is incorrect - if (!hasValidHeader(eventHeader)) { + if (!"system hook".equalsIgnoreCase(eventHeader)) { return Response.ok().build(); } + // based on event name in body, process the event hook String eventName = readEventName(jsonBody); EventType type = EventType.getType(eventName); - switch (type) { case PROJECT_CREATE: hookService.processProjectCreateHook(wrapper, convertRequestToHook(jsonBody)); break; - case PROJECT_DESTROY: hookService.processProjectDeleteHook(wrapper, convertRequestToHook(jsonBody)); break; - case PROJECT_RENAME: hookService.processProjectRenameHook(wrapper, convertRequestToHook(jsonBody)); break; - case UNSUPPORTED: default: LOGGER.trace("Dropped event: {}", eventName); @@ -76,8 +70,8 @@ public class WebhooksResource { } /** - * Processes the json body contents and converts them to a SystemHook object. - * Returns null if the json body couldn't be processed. + * Processes the json body contents and converts them to a SystemHook object. Returns null if the json body couldn't be + * processed. * * @param jsonBody the json body as a string * @return A SystemHook object converted from a json string @@ -92,9 +86,8 @@ public class WebhooksResource { } /** - * Processes the json body contents and returns the event_name field. Returns - * null if the was a processing error or if the event_name field is - * null/missing. + * Processes the json body contents and returns the event_name field. Returns null if the was a processing error or if + * the event_name field is null/missing. * * @param jsonBody the json body as a String * @return the event_name field @@ -107,14 +100,4 @@ public class WebhooksResource { return null; } } - - /** - * Validates that the hook header contains the required value - * - * @param eventHeader the event header value - * @return true if valid, false if not - */ - private boolean hasValidHeader(String eventHeader) { - return eventHeader != null && eventHeader.equalsIgnoreCase("system hook"); - } } diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java b/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java index 9efdc637..8c2d182b 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java @@ -2,8 +2,8 @@ package org.eclipsefoundation.git.eca.service; import java.util.List; -import org.eclipsefoundation.git.eca.model.InterestGroupData; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.InterestGroupData; +import org.eclipsefoundation.git.eca.api.models.Project; /** * Service for retrieving and interacting with interest groups. diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java index 925ddfda..54c2199f 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java @@ -13,7 +13,7 @@ package org.eclipsefoundation.git.eca.service; import java.util.List; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.namespace.ProviderType; diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/SystemHookService.java b/src/main/java/org/eclipsefoundation/git/eca/service/SystemHookService.java index 2ec8bdc5..3ddab46e 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/SystemHookService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/SystemHookService.java @@ -12,7 +12,7 @@ package org.eclipsefoundation.git.eca.service; import org.eclipsefoundation.core.model.RequestWrapper; -import org.eclipsefoundation.git.eca.model.SystemHook; +import org.eclipsefoundation.git.eca.api.models.SystemHook; /** * Processes the various system hooks received. diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java index 1c352bfc..0bb24842 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java @@ -13,8 +13,8 @@ package org.eclipsefoundation.git.eca.service; import java.util.List; -import org.eclipsefoundation.git.eca.api.models.EclipseUser; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.Project; +import org.eclipsefoundation.git.eca.model.EclipseUser; public interface UserService { diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java index 55b9cb90..0110228c 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java @@ -16,8 +16,8 @@ import java.security.NoSuchAlgorithmException; import java.util.List; import org.eclipsefoundation.core.model.RequestWrapper; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; -import org.eclipsefoundation.git.eca.model.Project; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.model.ValidationResponse; diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java index dc4427f0..54e84185 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java @@ -31,7 +31,7 @@ import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.AccountsAPI; import org.eclipsefoundation.git.eca.api.BotsAPI; import org.eclipsefoundation.git.eca.api.models.EclipseUser; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.service.OAuthService; import org.eclipsefoundation.git.eca.service.UserService; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java index 0fc98e65..b4c113c8 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java @@ -11,8 +11,8 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipsefoundation.core.service.APIMiddleware; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.ProjectsAPI; -import org.eclipsefoundation.git.eca.model.InterestGroupData; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.InterestGroupData; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.service.InterestGroupService; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultSystemHookService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultSystemHookService.java index f56aabf8..394f717a 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultSystemHookService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultSystemHookService.java @@ -34,8 +34,8 @@ import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.GitlabAPI; import org.eclipsefoundation.git.eca.api.models.GitlabProjectResponse; import org.eclipsefoundation.git.eca.api.models.GitlabUserResponse; +import org.eclipsefoundation.git.eca.api.models.SystemHook; import org.eclipsefoundation.git.eca.dto.PrivateProjectEvent; -import org.eclipsefoundation.git.eca.model.SystemHook; import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames; import org.eclipsefoundation.git.eca.service.SystemHookService; import org.eclipsefoundation.persistence.dao.PersistenceDao; diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java index a73f02af..4614ffd6 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java @@ -29,7 +29,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipsefoundation.core.service.APIMiddleware; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.ProjectsAPI; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.namespace.ProviderType; import org.eclipsefoundation.git.eca.service.InterestGroupService; diff --git a/src/main/resources/META-INF/resources/favicon.ico b/src/main/resources/META-INF/resources/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bb43f009ecaac5bac6feda26839f664145915392 GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x$A1^@s5XP^|L8!TOa(L%QC*<LXo z?g?7LqL1}NB>pS#3+$BO<&EIy;t*kHXJLZr0oe@$^7H?5$aFnjC0}>xiLt!KH$Mf9 z|IsS?|6^1Q{)a2+{&$jA`mZT0c9@rgEeNIuS-)K0>-h@p55Bsp82t}Z()pjRW%j>R z-{yarf$jf%9n1f5s)qml6*T{wh)F*e=H?0yRnlRE>6e}M)lY8X$KS5H4*!7S|Fg8r z|Cd1Zg7lXd*!(Zlv;H5YtoJ`yQTxA%xb$-;86|y?e)%b1gk`6E`)H9n=fke0EB}A~ z@Bt`{8@zh_<p1)Sv;P~2O7;U)G00B-Dla$r`&TCm=l>P{;s0BsQ~r0yr{ly8k%|9n zLu3Cd3kuB!>!0*3QFijT*PGX@{r~XBE!-9($$b0r<-ekU;7qXosb3Z3rhNP6s&Du| z-^B-LcFh0As3e>iWPe?F{C^of{wW~+pm>v=^8J%_&a^kjwrv0Z<>MzLn=qLmH+C16 z{s*RuN|1gK0EVHL?8Hw$9W~7Vg2Epk4pP+muOlM<L`gtM1g0NkhHU@)sj^+qzuD@! z;M5O^f2rHRG#CKVgRCE%{(4?*kZHd8)Ii_$J1G4jyTe{e@xQjP*iK*?wgKq{0c8CU z5n%pHUM4Qva%T}N{R89qBPjig@$f_laB&F1)WP(F%osS#!0<<&f#Cx?1H%JmAZBD> XH~@4Q3I>XS#6j{Pbs+ViXaivYmUJiX literal 0 HcmV?d00001 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3ffafdbf..4233e7df 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -36,6 +36,16 @@ quarkus.cache.caffeine."default".expire-after-write=1H quarkus.cache.caffeine."record".initial-capacity=${quarkus.cache.caffeine."default".initial-capacity} quarkus.cache.caffeine."record".expire-after-write=${quarkus.cache.caffeine."default".expire-after-write} +## JWT cache, 115 second cache time to make sure there is no accidental sending of an expired token +quarkus.cache.caffeine."accesstoken".initial-capacity=1000 +quarkus.cache.caffeine."accesstoken".expire-after-write=119M +## JWT Placeholders/defaults +smallrye.jwt.encrypt.relax-key-validation=true +smallrye.jwt.new-token.lifespan=120 +smallrye.jwt.sign.key.location=/var/run/secrets/pk.pem +smallrye.jwt.new-token.issuer=12345 +eclipse.webhooks.github.context=eclipsefdn/eca + eclipse.mail.allowlist=noreply@github.com,49699333+dependabot[bot]@users.noreply.github.com %dev.eclipse.cache.resource.enabled=true %dev.eclipse.optional-resources.enabled=true diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java index dd0f657b..23a1d312 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/resource/WebhoooksResourceTest.java @@ -16,8 +16,8 @@ import java.util.Arrays; import java.util.Map; import java.util.Optional; -import org.eclipsefoundation.git.eca.model.SystemHook; -import org.eclipsefoundation.git.eca.model.SystemHook.Owner; +import org.eclipsefoundation.git.eca.api.models.SystemHook; +import org.eclipsefoundation.git.eca.api.models.SystemHook.Owner; import org.eclipsefoundation.testing.helpers.TestCaseHelper; import org.eclipsefoundation.testing.templates.RestAssuredTemplates; import org.eclipsefoundation.testing.templates.RestAssuredTemplates.EndpointTestCase; diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java index a59776fe..434fcec2 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java @@ -19,7 +19,7 @@ import java.util.Optional; import javax.inject.Inject; import org.eclipsefoundation.git.eca.api.models.EclipseUser; -import org.eclipsefoundation.git.eca.model.Project; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.service.ProjectsService; import org.eclipsefoundation.git.eca.service.UserService; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java index 0ccade49..3ebbabce 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java +++ b/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java @@ -16,8 +16,8 @@ import java.util.stream.Collectors; import javax.inject.Inject; -import org.eclipsefoundation.git.eca.model.Project; -import org.eclipsefoundation.git.eca.model.Project.Repo; +import org.eclipsefoundation.git.eca.api.models.Project; +import org.eclipsefoundation.git.eca.api.models.Project.Repo; import org.eclipsefoundation.git.eca.service.ProjectsService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java index 103487d5..47724820 100644 --- a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java +++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java @@ -25,12 +25,12 @@ import javax.ws.rs.core.Response; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters; import org.eclipsefoundation.git.eca.api.ProjectsAPI; -import org.eclipsefoundation.git.eca.model.InterestGroupData; -import org.eclipsefoundation.git.eca.model.InterestGroupData.Descriptor; -import org.eclipsefoundation.git.eca.model.Project; -import org.eclipsefoundation.git.eca.model.Project.GitlabProject; -import org.eclipsefoundation.git.eca.model.Project.Repo; -import org.eclipsefoundation.git.eca.model.Project.User; +import org.eclipsefoundation.git.eca.api.models.InterestGroupData; +import org.eclipsefoundation.git.eca.api.models.Project; +import org.eclipsefoundation.git.eca.api.models.InterestGroupData.Descriptor; +import org.eclipsefoundation.git.eca.api.models.Project.GitlabProject; +import org.eclipsefoundation.git.eca.api.models.Project.Repo; +import org.eclipsefoundation.git.eca.api.models.Project.User; import io.quarkus.test.Mock; -- GitLab From 3a01be39246c0b1267022f314f8bacccea764bea Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Fri, 18 Nov 2022 11:41:36 -0500 Subject: [PATCH 02/10] Add BouncyCastle to enable PKCS#1 parsing, add manual parsing helper --- pom.xml | 11 ++++ .../eca/api/models/GithubWebhookRequest.java | 2 - .../git/eca/config/BCSecurityProvider.java | 37 ++++++++++++ .../git/eca/helper/JwtHelper.java | 58 +++++++++++++++++++ .../eca/resource/GithubWebhooksResource.java | 20 ++++--- .../impl/PaginationProjectsService.java | 14 +++-- src/main/resources/application.properties | 3 +- 7 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/eclipsefoundation/git/eca/config/BCSecurityProvider.java create mode 100644 src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java diff --git a/pom.xml b/pom.xml index 6708f7d4..3baf3ec6 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,17 @@ <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + </dependency> + <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on --> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <version>1.70</version> + </dependency> + <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-rest-client</artifactId> diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java index df829081..7c0e93ac 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java @@ -118,7 +118,6 @@ public abstract class GithubWebhookRequest { @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_Installation.Builder.class) public abstract static class Installation { public abstract String getId(); - public abstract String getWebhookId(); public static Builder builder() { return new AutoValue_GithubWebhookRequest_Installation.Builder(); @@ -128,7 +127,6 @@ public abstract class GithubWebhookRequest { @JsonPOJOBuilder(withPrefix = "set") public abstract static class Builder { public abstract Builder setId(String id); - public abstract Builder setWebhookId(String webhookId); public abstract Installation build(); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/BCSecurityProvider.java b/src/main/java/org/eclipsefoundation/git/eca/config/BCSecurityProvider.java new file mode 100644 index 00000000..ee86744b --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/config/BCSecurityProvider.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.config; + +import java.security.Security; + +import javax.annotation.PostConstruct; +import javax.inject.Singleton; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import io.quarkus.runtime.Startup; + +/** + * Adds Bouncycastle (BC) as a security provider to enable PKCS1 consumption. + * + * @author Martin Lowe + * + */ +@Startup +@Singleton +public class BCSecurityProvider { + + @PostConstruct + public void init() { + Security.addProvider(new BouncyCastleProvider()); + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java new file mode 100644 index 00000000..4a737d73 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2022 Eclipse Foundation + * + * 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/ + * + * Author: Martin Lowe <martin.lowe@eclipse-foundation.org> + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.git.eca.helper; + +import java.io.FileReader; +import java.nio.file.Paths; +import java.security.PrivateKey; + +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper to load external resources as a JWT key. + * + * @author Martin Lowe + * + */ +public class JwtHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtHelper.class); + + /** + * Reads in external PEM keys in a way that supports both PKCS#1 and PKCS#8. This is needed as the GH-provided RSA key + * is encoded using PKCS#1 and is not available for consumption in OOTB Java/smallrye, and smallrye's default PEM reader + * only reads within the resources path, so external file support isn't really available. + * + * Src: https://stackoverflow.com/questions/41934846/read-rsa-private-key-of-format-pkcs1-in-java + * + * @param location the location of the file + * @return the PrivateKey instance for the PEM file at the location, or null if it could not be read/parsed. + */ + public static PrivateKey getExternalPrivateKey(String location) { + // create auto-closing reading resources for the external PEM file + try (FileReader keyReader = new FileReader(Paths.get(location).toFile()); PEMParser pemParser = new PEMParser(keyReader)) { + // use the BouncyCastle provider for PKCS#1 support (not available ootb) + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + // create the key and retrieve the PrivateKey portion + return converter.getKeyPair((PEMKeyPair) pemParser.readObject()).getPrivate(); + } catch (Exception e) { + LOGGER.error("Error while loading private pem", e); + } + return null; + } + + private JwtHelper() { + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index a9245a6f..1477cc3a 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -12,9 +12,6 @@ package org.eclipsefoundation.git.eca.resource; import java.net.URI; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.List; import java.util.stream.Collectors; @@ -34,6 +31,7 @@ import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; import org.eclipsefoundation.git.eca.api.models.GithubCommit; import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest; +import org.eclipsefoundation.git.eca.helper.JwtHelper; import org.eclipsefoundation.git.eca.model.Commit; import org.eclipsefoundation.git.eca.model.GitUser; import org.eclipsefoundation.git.eca.model.ValidationRequest; @@ -45,7 +43,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkus.cache.CacheResult; -import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.auth.principal.JWTParser; import io.smallrye.jwt.auth.principal.ParseException; import io.smallrye.jwt.build.Jwt; @@ -58,6 +55,8 @@ import io.smallrye.jwt.build.Jwt; public class GithubWebhooksResource { private static final Logger LOGGER = LoggerFactory.getLogger(GithubWebhooksResource.class); + @ConfigProperty(name = "smallrye.jwt.sign.key.location") + String location; @ConfigProperty(name = "eclipse.webhooks.github.secret") String secret; @ConfigProperty(name = "eclipse.webhooks.github.context") @@ -79,8 +78,7 @@ public class GithubWebhooksResource { @GET @Path("jwt/{installationId}") public Response test(@PathParam("installationId") String installId) throws ParseException { - - return Response.ok(parser.verify(generateJwt(), secret)).build(); + return Response.ok(parser.parse(generateJwt())).build(); } @POST @@ -91,6 +89,8 @@ public class GithubWebhooksResource { if (!"pull_request".equalsIgnoreCase(eventType)) { return Response.ok().build(); } + // TODO set to debug, this would be spammy as all get out + LOGGER.warn("Processing PR event for install {} with delivery ID of {}", request.getInstallation().getId(), deliveryId); // start validation process ValidationRequest vr = generateRequest(request); String fingerprint = validationService.generateRequestHash(vr); @@ -166,6 +166,12 @@ public class GithubWebhooksResource { .build(); } + /** + * Retrieve a new cached Github access token, using a JWT generated from the GH provided PKCS#1 private key. + * + * @param installationId the installation that the access token is being generated for + * @return the access token to be used in the requests. + */ @CacheResult(cacheName = "accesstoken") protected GithubAccessToken getAccessToken(String installationId) { return ghApi.getNewAccessToken("Bearer " + generateJwt(), installationId); @@ -178,6 +184,6 @@ public class GithubWebhooksResource { * @return signed JWT using the issuer and secret defined in the secret properties. */ private String generateJwt() { - return Jwt.subject("EclipseWebmaster").signWithSecret(secret); + return Jwt.subject("EclipseWebmaster").sign(JwtHelper.getExternalPrivateKey(location)); } } diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java index 4614ffd6..d360d2e0 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java @@ -58,6 +58,8 @@ import io.quarkus.runtime.Startup; public class PaginationProjectsService implements ProjectsService { private static final Logger LOGGER = LoggerFactory.getLogger(PaginationProjectsService.class); + @ConfigProperty(name = "eclipse.projects.precache.enabled", defaultValue = "true") + boolean isEnabled; @ConfigProperty(name = "cache.pagination.refresh-frequency-seconds", defaultValue = "3600") long refreshAfterWrite; @@ -117,12 +119,14 @@ public class PaginationProjectsService implements ProjectsService { } }); - // pre-cache the projects to reduce load time for other users - LOGGER.debug("Starting pre-cache of projects"); - if (getProjects() == null) { - LOGGER.warn("Unable to populate pre-cache for Eclipse projects. Calls may experience degraded performance."); + if (isEnabled) { + // pre-cache the projects to reduce load time for other users + LOGGER.debug("Starting pre-cache of projects"); + if (getProjects() == null) { + LOGGER.warn("Unable to populate pre-cache for Eclipse projects. Calls may experience degraded performance."); + } + LOGGER.debug("Completed pre-cache of projects assets"); } - LOGGER.debug("Completed pre-cache of projects assets"); } @Override diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4233e7df..75ac0f30 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -42,8 +42,7 @@ quarkus.cache.caffeine."accesstoken".expire-after-write=119M ## JWT Placeholders/defaults smallrye.jwt.encrypt.relax-key-validation=true smallrye.jwt.new-token.lifespan=120 -smallrye.jwt.sign.key.location=/var/run/secrets/pk.pem -smallrye.jwt.new-token.issuer=12345 +smallrye.jwt.new-token.issuer=262450 eclipse.webhooks.github.context=eclipsefdn/eca eclipse.mail.allowlist=noreply@github.com,49699333+dependabot[bot]@users.noreply.github.com -- GitLab From f308ffed9e6167dec84bcf1bca399f2ac182011b Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Fri, 18 Nov 2022 16:15:05 -0500 Subject: [PATCH 03/10] Add temp log statement to expose installation access token on fetch --- .../git/eca/resource/GithubWebhooksResource.java | 6 ++++-- src/test/resources/application.properties | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index 1477cc3a..7fa081ae 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -57,8 +57,6 @@ public class GithubWebhooksResource { @ConfigProperty(name = "smallrye.jwt.sign.key.location") String location; - @ConfigProperty(name = "eclipse.webhooks.github.secret") - String secret; @ConfigProperty(name = "eclipse.webhooks.github.context") String context; @@ -115,6 +113,10 @@ public class GithubWebhooksResource { * @param fingerprint */ private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state, String fingerprint) { + // TODO remove me, used in testing on staging to expose token + LOGGER + .warn("Generated access token for installation {}: {}", request.getInstallation().getId(), + getAccessToken(request.getInstallation().getId()).getToken()); ghApi .updateStatus("Bearer " + getAccessToken(request.getInstallation().getId()), request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(), diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 78bcff0f..b127c9e0 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -25,3 +25,4 @@ quarkus.http.port=8080 quarkus.oidc.enabled=false quarkus.keycloak.devservices.enabled=false quarkus.oidc-client.enabled=false +smallrye.jwt.sign.key.location=test.pem \ No newline at end of file -- GitLab From 6dc4e52effd740f43ed1b44d89c28c124b2115b6 Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Mon, 21 Nov 2022 13:33:58 -0500 Subject: [PATCH 04/10] Update GH commits fetch to have authentication for webhook processing --- .../java/org/eclipsefoundation/git/eca/api/GithubAPI.java | 6 ++++-- .../git/eca/resource/GithubWebhooksResource.java | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java index dfe002f6..ac573cfc 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java @@ -33,7 +33,8 @@ public interface GithubAPI { @GET @Path("repos/{repoFull}/pulls/{pullNumber}/commits") - public Response getCommits(@PathParam("repoFull") String repoFull, @PathParam("pullNumber") int pullNumber); + public Response getCommits(@HeaderParam("Authorization") String bearer, @PathParam("repoFull") String repoFull, + @PathParam("pullNumber") int pullNumber); @POST @Path("repos/{repoFull}/statuses/{prHeadSha}") @@ -42,5 +43,6 @@ public interface GithubAPI { @POST @Path("app/installations/{installationId}/access_tokens") - public GithubAccessToken getNewAccessToken(@HeaderParam("Authorization") String bearer, @PathParam("installationId") String installationId); + public GithubAccessToken getNewAccessToken(@HeaderParam("Authorization") String bearer, + @PathParam("installationId") String installationId); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index 7fa081ae..13f70772 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -136,9 +136,14 @@ public class GithubWebhooksResource { * @return the Validation Request body to be used in validating data */ private ValidationRequest generateRequest(GithubWebhookRequest request) { + LOGGER + .warn("Generated access token for installation {}: {}", request.getInstallation().getId(), + getAccessToken(request.getInstallation().getId()).getToken()); // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful List<GithubCommit> commits = middleware - .getAll(i -> ghApi.getCommits(request.getRepository().getFullName(), request.getPullRequest().getNumber()), + .getAll(i -> ghApi + .getCommits("Bearer " + getAccessToken(request.getInstallation().getId()).getToken(), + request.getRepository().getFullName(), request.getPullRequest().getNumber()), GithubCommit.class); LOGGER .trace("Retrieved {} commits for PR {} in repo {}", commits.size(), request.getPullRequest().getNumber(), -- GitLab From 8af384715f1cbc0ee12c7a3fe83ff285dc958863 Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Tue, 22 Nov 2022 15:27:00 -0500 Subject: [PATCH 05/10] Update auth headers for GH api client --- .../java/org/eclipsefoundation/git/eca/api/GithubAPI.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java index ac573cfc..db9abae0 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java @@ -33,16 +33,16 @@ public interface GithubAPI { @GET @Path("repos/{repoFull}/pulls/{pullNumber}/commits") - public Response getCommits(@HeaderParam("Authorization") String bearer, @PathParam("repoFull") String repoFull, + public Response getCommits(@HeaderParam("authorization") String bearer, @PathParam("repoFull") String repoFull, @PathParam("pullNumber") int pullNumber); @POST @Path("repos/{repoFull}/statuses/{prHeadSha}") - public Response updateStatus(@HeaderParam("Authorization") String bearer, @PathParam("repoFull") String repoFull, + public Response updateStatus(@HeaderParam("authorization") String bearer, @PathParam("repoFull") String repoFull, @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate); @POST @Path("app/installations/{installationId}/access_tokens") - public GithubAccessToken getNewAccessToken(@HeaderParam("Authorization") String bearer, + public GithubAccessToken getNewAccessToken(@HeaderParam("authorization") String bearer, @PathParam("installationId") String installationId); } -- GitLab From 585d2f99542b26266c54279eabcfc56044bbdcb4 Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Tue, 22 Nov 2022 15:37:18 -0500 Subject: [PATCH 06/10] Fix status bearer token using object instead of token as bearer token Default functionality when you concat a string and an object is that the JVM will call toString on an object, which is not the correct functionality. We need only the token in this case, so an intermediary method was created. --- .../git/eca/resource/GithubWebhooksResource.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index 13f70772..8a019d4e 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -118,7 +118,7 @@ public class GithubWebhooksResource { .warn("Generated access token for installation {}: {}", request.getInstallation().getId(), getAccessToken(request.getInstallation().getId()).getToken()); ghApi - .updateStatus("Bearer " + getAccessToken(request.getInstallation().getId()), request.getRepository().getFullName(), + .updateStatus(getBearerString(request.getInstallation().getId()), request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(), GithubCommitStatusRequest .builder() @@ -138,12 +138,12 @@ public class GithubWebhooksResource { private ValidationRequest generateRequest(GithubWebhookRequest request) { LOGGER .warn("Generated access token for installation {}: {}", request.getInstallation().getId(), - getAccessToken(request.getInstallation().getId()).getToken()); + getBearerString(request.getInstallation().getId())); // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful List<GithubCommit> commits = middleware .getAll(i -> ghApi - .getCommits("Bearer " + getAccessToken(request.getInstallation().getId()).getToken(), - request.getRepository().getFullName(), request.getPullRequest().getNumber()), + .getCommits(getBearerString(request.getInstallation().getId()), request.getRepository().getFullName(), + request.getPullRequest().getNumber()), GithubCommit.class); LOGGER .trace("Retrieved {} commits for PR {} in repo {}", commits.size(), request.getPullRequest().getNumber(), @@ -173,6 +173,10 @@ public class GithubWebhooksResource { .build(); } + protected String getBearerString(String installationId) { + return "Bearer " + getAccessToken(installationId).getToken(); + } + /** * Retrieve a new cached Github access token, using a JWT generated from the GH provided PKCS#1 private key. * -- GitLab From 1c09ed8fa810b22f70d8fd8d7e0ca1ec407e1090 Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Fri, 25 Nov 2022 13:16:26 -0500 Subject: [PATCH 07/10] Update GH webhook resource to bind to validation service --- .../eca/resource/GithubWebhooksResource.java | 29 +++++++----- .../git/eca/resource/ValidationResource.java | 47 ++++++++----------- .../git/eca/service/UserService.java | 2 +- .../impl/DefaultValidationService.java | 2 +- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index 8a019d4e..b8e0f93c 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -35,6 +35,7 @@ import org.eclipsefoundation.git.eca.helper.JwtHelper; import org.eclipsefoundation.git.eca.model.Commit; import org.eclipsefoundation.git.eca.model.GitUser; import org.eclipsefoundation.git.eca.model.ValidationRequest; +import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.namespace.GithubCommitStatuses; import org.eclipsefoundation.git.eca.namespace.ProviderType; import org.eclipsefoundation.git.eca.service.ValidationService; @@ -48,7 +49,9 @@ import io.smallrye.jwt.auth.principal.ParseException; import io.smallrye.jwt.build.Jwt; /** - * @author martin + * Resource for processing Github pull request events, used to update commit status entries for the Git ECA application. + * + * @author Martin Lowe * */ @Path("webhooks/github") @@ -87,17 +90,15 @@ public class GithubWebhooksResource { if (!"pull_request".equalsIgnoreCase(eventType)) { return Response.ok().build(); } - // TODO set to debug, this would be spammy as all get out - LOGGER.warn("Processing PR event for install {} with delivery ID of {}", request.getInstallation().getId(), deliveryId); + LOGGER.trace("Processing PR event for install {} with delivery ID of {}", request.getInstallation().getId(), deliveryId); // start validation process ValidationRequest vr = generateRequest(request); String fingerprint = validationService.generateRequestHash(vr); updateCommitStatus(request, GithubCommitStatuses.PENDING, fingerprint); - LOGGER.debug("Would have validated: {}", vr); - // TODO once validation move update done, use vr above to validate - boolean hasPassed = true; - if (hasPassed) { + // validate the response + ValidationResponse r = validationService.validateIncomingRequest(vr, wrapper); + if (r.getPassed()) { updateCommitStatus(request, GithubCommitStatuses.SUCCESS, fingerprint); } else { updateCommitStatus(request, GithubCommitStatuses.FAILURE, fingerprint); @@ -110,12 +111,11 @@ public class GithubWebhooksResource { * * @param request the current webhook update request * @param state the state to set the status to - * @param fingerprint + * @param fingerprint the internal unique string for the set of commits being processed */ private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state, String fingerprint) { - // TODO remove me, used in testing on staging to expose token LOGGER - .warn("Generated access token for installation {}: {}", request.getInstallation().getId(), + .trace("Generated access token for installation {}: {}", request.getInstallation().getId(), getAccessToken(request.getInstallation().getId()).getToken()); ghApi .updateStatus(getBearerString(request.getInstallation().getId()), request.getRepository().getFullName(), @@ -136,9 +136,6 @@ public class GithubWebhooksResource { * @return the Validation Request body to be used in validating data */ private ValidationRequest generateRequest(GithubWebhookRequest request) { - LOGGER - .warn("Generated access token for installation {}: {}", request.getInstallation().getId(), - getBearerString(request.getInstallation().getId())); // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful List<GithubCommit> commits = middleware .getAll(i -> ghApi @@ -173,6 +170,12 @@ public class GithubWebhooksResource { .build(); } + /** + * Retrieve the bearer token string for the current installation. + * + * @param installationId the installation to generate a bearer string for + * @return the bearer string for the current request + */ protected String getBearerString(String installationId) { return "Bearer " + getAccessToken(installationId).getToken(); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java index d164c0af..7985ad0a 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java @@ -30,12 +30,9 @@ import javax.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.core.service.CachingService; +import org.eclipsefoundation.git.eca.api.models.EclipseUser; import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; -import org.eclipsefoundation.git.eca.helper.CommitHelper; -import org.eclipsefoundation.git.eca.model.Commit; -import org.eclipsefoundation.git.eca.model.EclipseUser; -import org.eclipsefoundation.git.eca.model.GitUser; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.namespace.APIStatusCode; @@ -51,9 +48,8 @@ import io.quarkus.qute.Location; import io.quarkus.qute.Template; /** - * ECA validation endpoint for Git commits. Will use information from the bots, - * projects, and accounts API to validate commits passed to this endpoint. - * Should be as system agnostic as possible to allow for any service to request + * ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate + * commits passed to this endpoint. Should be as system agnostic as possible to allow for any service to request * validation with less reliance on services external to the Eclipse foundation. * * @author Martin Lowe, Zachary Sabourin @@ -91,16 +87,14 @@ public class ValidationResource { Template membershipTemplateWeb; /** - * 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 - * ensure that all users are covered either by an ECA, or are committers on the - * project. In the case of ECA-only contributors, an additional sign off footer - * is required in the body of the commit. + * 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 ensure that all users are covered either by an ECA, or are committers on + * the project. In the case of ECA-only contributors, an additional sign off footer is required in the body of the + * commit. * * @param req the request containing basic data plus the commits to be validated - * @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. + * @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 @@ -113,7 +107,7 @@ public class ValidationResource { } else { // create a stubbed response with the errors ValidationResponse out = ValidationResponse.builder().build(); - messages.forEach(m -> out.addError(m, null,APIStatusCode.ERROR_DEFAULT)); + messages.forEach(m -> out.addError(m, null, APIStatusCode.ERROR_DEFAULT)); return out.toResponse(); } } @@ -132,18 +126,19 @@ public class ValidationResource { if (statuses.isEmpty()) { return Response.status(404).build(); } - List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), - statuses.get(0).getProvider()); - return Response.ok().entity(membershipTemplateWeb.data("statuses", statuses, "repoUrl", - statuses.get(0).getRepoUrl(), "project", ps.isEmpty() ? null : ps.get(0)).render()).build(); + List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider()); + return Response + .ok() + .entity(membershipTemplateWeb + .data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl(), "project", ps.isEmpty() ? null : ps.get(0)) + .render()) + .build(); } @GET @Path("/lookup") public Response getUserStatus(@QueryParam("email") String email) { - EclipseUser user = users.getUser(email); - if (Objects.isNull(user)) { return Response.status(404).build(); } @@ -151,17 +146,15 @@ public class ValidationResource { if (!user.getECA().getSigned()) { return Response.status(403).build(); } - return Response.ok().build(); } /** - * Check if there are any issues with the validation request, returning error - * messages if there are issues with the request. + * Check if there are any issues with the validation request, returning error messages if there are issues with the + * request. * * @param req the current validation request - * @return a list of error messages to report, or an empty list if there are no - * errors with the request. + * @return a list of error messages to report, or an empty list if there are no errors with the request. */ private List<String> checkRequest(ValidationRequest req) { // check that we have commits to validate diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java index 0bb24842..06c01484 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java @@ -13,8 +13,8 @@ package org.eclipsefoundation.git.eca.service; import java.util.List; +import org.eclipsefoundation.git.eca.api.models.EclipseUser; import org.eclipsefoundation.git.eca.api.models.Project; -import org.eclipsefoundation.git.eca.model.EclipseUser; public interface UserService { diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java index 1d09d596..effafd29 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java @@ -29,13 +29,13 @@ import org.eclipsefoundation.core.helper.DateTimeHelper; import org.eclipsefoundation.core.model.RequestWrapper; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.git.eca.api.models.EclipseUser; +import org.eclipsefoundation.git.eca.api.models.Project; import org.eclipsefoundation.git.eca.dto.CommitValidationMessage; import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; import org.eclipsefoundation.git.eca.dto.CommitValidationStatusGrouping; import org.eclipsefoundation.git.eca.helper.CommitHelper; import org.eclipsefoundation.git.eca.model.Commit; import org.eclipsefoundation.git.eca.model.GitUser; -import org.eclipsefoundation.git.eca.model.Project; import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.namespace.APIStatusCode; -- GitLab From 116d7914005908e41d990e326c54611689da316a Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Fri, 20 Jan 2023 11:22:53 -0500 Subject: [PATCH 08/10] Clean up the classes for GH hook binding --- .../git/eca/api/GithubAPI.java | 4 +++- .../git/eca/model/PrivateProjectData.java | 6 ++++++ .../eca/model/mappers/BaseEntityMapper.java | 1 - .../eca/namespace/GithubCommitStatuses.java | 15 ++++++++++++- .../eca/resource/GithubWebhooksResource.java | 21 +++++++++++-------- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java index db9abae0..a6ddea0c 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java @@ -24,7 +24,9 @@ import org.eclipsefoundation.git.eca.api.models.GithubAccessToken; import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest; /** - * @author martin + * Bindings used by the webhook logic to retrieve data about the PR to be validated. + * + * @author Martin Lowe * */ @RegisterRestClient(baseUri = "https://api.github.com") diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/PrivateProjectData.java b/src/main/java/org/eclipsefoundation/git/eca/model/PrivateProjectData.java index 2a4215a4..b46f7b58 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/PrivateProjectData.java +++ b/src/main/java/org/eclipsefoundation/git/eca/model/PrivateProjectData.java @@ -20,6 +20,12 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.auto.value.AutoValue; +/** + * Model for Gitlab private project report logs. Used to report on historic projects in the EF hosted Gitlab instance. + * + * @author Zachary Sabourin + * + */ @AutoValue @JsonDeserialize(builder = AutoValue_PrivateProjectData.Builder.class) public abstract class PrivateProjectData { diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/mappers/BaseEntityMapper.java b/src/main/java/org/eclipsefoundation/git/eca/model/mappers/BaseEntityMapper.java index 145e7607..0961f0fa 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/model/mappers/BaseEntityMapper.java +++ b/src/main/java/org/eclipsefoundation/git/eca/model/mappers/BaseEntityMapper.java @@ -15,7 +15,6 @@ import org.eclipsefoundation.persistence.dao.PersistenceDao; import org.eclipsefoundation.persistence.dto.BareNode; import org.mapstruct.Context; import org.mapstruct.InheritInverseConfiguration; -import org.mapstruct.MapperConfig; import org.mapstruct.ObjectFactory; import org.mapstruct.TargetType; diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java index 85f7c758..2017fca8 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java +++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GithubCommitStatuses.java @@ -12,7 +12,10 @@ package org.eclipsefoundation.git.eca.namespace; /** - * @author martin + * Statuses used for Github commit status objects. Contains basic messaging used in the description of the status to be + * sent back to Github. + * + * @author Martin Lowe * */ public enum GithubCommitStatuses { @@ -24,10 +27,20 @@ public enum GithubCommitStatuses { private String message; + /** + * Instantiate the enum status with the display message. + * + * @param message the display message associated with the status. + */ private GithubCommitStatuses(String message) { this.message = message; } + /** + * Returns the message associated with the status. + * + * @return the string message for status. + */ public String getMessage() { return this.message; } diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index b8e0f93c..cf44c86d 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -16,10 +16,8 @@ import java.util.List; import java.util.stream.Collectors; import javax.inject.Inject; -import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -45,7 +43,6 @@ import org.slf4j.LoggerFactory; import io.quarkus.cache.CacheResult; import io.smallrye.jwt.auth.principal.JWTParser; -import io.smallrye.jwt.auth.principal.ParseException; import io.smallrye.jwt.build.Jwt; /** @@ -76,12 +73,18 @@ public class GithubWebhooksResource { @RestClient GithubAPI ghApi; - @GET - @Path("jwt/{installationId}") - public Response test(@PathParam("installationId") String installId) throws ParseException { - return Response.ok(parser.parse(generateJwt())).build(); - } - + /** + * Entry point for processing Github webhook requests. Accepts standard fields as described in the <a href= + * "https://docs.github.com/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#webhook-payload-object-common-properties">Webhook + * properties documentation</a>, as well as the <a href= + * "https://docs.github.com/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request">Pull + * Request event type documentation.</a> + * + * @param deliveryId Github provided unique delivery ID + * @param eventType the type of webhook event that was fired + * @param request the webhook request body. + * @return an OK status when done processing. + */ @POST @Path("check") public Response processGithubCheck(@HeaderParam("X-GitHub-Delivery") String deliveryId, @HeaderParam("X-GitHub-Event") String eventType, -- GitLab From a2a3db3e7b723f3a1d5836e9146571468b309a1c Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Mon, 6 Feb 2023 14:45:38 -0500 Subject: [PATCH 09/10] Update GH webhook target to be configurable --- .../git/eca/resource/GithubWebhooksResource.java | 4 +++- src/main/resources/application.properties | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index cf44c86d..c4873147 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -59,6 +59,8 @@ public class GithubWebhooksResource { String location; @ConfigProperty(name = "eclipse.webhooks.github.context") String context; + @ConfigProperty(name = "eclipse.webhooks.github.server-target") + String serverTarget; @Inject RequestWrapper wrapper; @@ -127,7 +129,7 @@ public class GithubWebhooksResource { .builder() .setDescription(state.getMessage()) .setState(state.toString()) - .setTargetUrl("https://api.eclipse.org/git/eca/status/" + fingerprint + "/ui") + .setTargetUrl(serverTarget + "/git/eca/status/" + fingerprint + "/ui") .setContext(context) .build()); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 75ac0f30..54f0870c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -44,6 +44,7 @@ smallrye.jwt.encrypt.relax-key-validation=true smallrye.jwt.new-token.lifespan=120 smallrye.jwt.new-token.issuer=262450 eclipse.webhooks.github.context=eclipsefdn/eca +eclipse.webhooks.github.server-target=https://api.eclipse.org eclipse.mail.allowlist=noreply@github.com,49699333+dependabot[bot]@users.noreply.github.com %dev.eclipse.cache.resource.enabled=true -- GitLab From 0fce3d071b5877053dd4d7bbbfb139106222fc37 Mon Sep 17 00:00:00 2001 From: Martin Lowe <martin.lowe@eclipse-foundation.org> Date: Tue, 7 Feb 2023 14:07:22 -0500 Subject: [PATCH 10/10] Update GH API binding to include a base version string Can be overridden once we get a value in the common API --- .../org/eclipsefoundation/git/eca/api/GithubAPI.java | 10 +++++----- .../git/eca/resource/GithubWebhooksResource.java | 9 ++++++--- src/main/resources/application.properties | 3 +-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java index a6ddea0c..d68e938c 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java +++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java @@ -35,16 +35,16 @@ public interface GithubAPI { @GET @Path("repos/{repoFull}/pulls/{pullNumber}/commits") - public Response getCommits(@HeaderParam("authorization") String bearer, @PathParam("repoFull") String repoFull, - @PathParam("pullNumber") int pullNumber); + public Response getCommits(@HeaderParam("authorization") String bearer, @HeaderParam("X-GitHub-Api-Version") String apiVersion, + @PathParam("repoFull") String repoFull, @PathParam("pullNumber") int pullNumber); @POST @Path("repos/{repoFull}/statuses/{prHeadSha}") - public Response updateStatus(@HeaderParam("authorization") String bearer, @PathParam("repoFull") String repoFull, - @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate); + public Response updateStatus(@HeaderParam("authorization") String bearer, @HeaderParam("X-GitHub-Api-Version") String apiVersion, + @PathParam("repoFull") String repoFull, @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate); @POST @Path("app/installations/{installationId}/access_tokens") public GithubAccessToken getNewAccessToken(@HeaderParam("authorization") String bearer, - @PathParam("installationId") String installationId); + @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("installationId") String installationId); } diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java index c4873147..26685b2d 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java @@ -62,6 +62,9 @@ public class GithubWebhooksResource { @ConfigProperty(name = "eclipse.webhooks.github.server-target") String serverTarget; + @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28") + String apiVersion; + @Inject RequestWrapper wrapper; @Inject @@ -123,7 +126,7 @@ public class GithubWebhooksResource { .trace("Generated access token for installation {}: {}", request.getInstallation().getId(), getAccessToken(request.getInstallation().getId()).getToken()); ghApi - .updateStatus(getBearerString(request.getInstallation().getId()), request.getRepository().getFullName(), + .updateStatus(getBearerString(request.getInstallation().getId()), apiVersion, request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(), GithubCommitStatusRequest .builder() @@ -144,7 +147,7 @@ public class GithubWebhooksResource { // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful List<GithubCommit> commits = middleware .getAll(i -> ghApi - .getCommits(getBearerString(request.getInstallation().getId()), request.getRepository().getFullName(), + .getCommits(getBearerString(request.getInstallation().getId()), apiVersion, request.getRepository().getFullName(), request.getPullRequest().getNumber()), GithubCommit.class); LOGGER @@ -193,7 +196,7 @@ public class GithubWebhooksResource { */ @CacheResult(cacheName = "accesstoken") protected GithubAccessToken getAccessToken(String installationId) { - return ghApi.getNewAccessToken("Bearer " + generateJwt(), installationId); + return ghApi.getNewAccessToken("Bearer " + generateJwt(), apiVersion, installationId); } /** diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 54f0870c..6d7f3dda 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -38,9 +38,8 @@ quarkus.cache.caffeine."record".expire-after-write=${quarkus.cache.caffeine."def ## JWT cache, 115 second cache time to make sure there is no accidental sending of an expired token quarkus.cache.caffeine."accesstoken".initial-capacity=1000 -quarkus.cache.caffeine."accesstoken".expire-after-write=119M +quarkus.cache.caffeine."accesstoken".expire-after-write=119S ## JWT Placeholders/defaults -smallrye.jwt.encrypt.relax-key-validation=true smallrye.jwt.new-token.lifespan=120 smallrye.jwt.new-token.issuer=262450 eclipse.webhooks.github.context=eclipsefdn/eca -- GitLab