From fe2491f97f7aca0803926ac370a89d189035bf4a Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Wed, 13 Nov 2019 06:33:34 -0800 Subject: [PATCH] Add support for global/project level webhooks (#10) * Add support for global/project level webhooks * Check for invalid scope, repository, and project * Improve exception handling * HMAC signing + JSON endpoint * Update src/main/java/com/sourcegraph/webhook/Dispatcher.java Co-Authored-By: Nick Snyder * Address comments * Fix ending newlines * Clean imports * Add ReopenedEvent * Support webhook delete by name + change header signature name * Exclude secret from being serialized --- pom.xml | 19 ++- .../com/sourcegraph/webhook/Dispatcher.java | 106 ++++++++++++ .../sourcegraph/webhook/EventSerializer.java | 114 +++++++++++++ .../sourcegraph/webhook/WebhookListener.java | 71 ++++++++ .../sourcegraph/webhook/WebhookRouter.java | 60 +++++++ .../webhook/registry/EventEntity.java | 13 ++ .../sourcegraph/webhook/registry/Webhook.java | 32 ++++ .../webhook/registry/WebhookEntity.java | 19 +++ .../webhook/registry/WebhookException.java | 51 ++++++ .../webhook/registry/WebhookRegistry.java | 157 ++++++++++++++++++ src/main/resources/atlassian-plugin.xml | 10 +- 11 files changed, 648 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/sourcegraph/webhook/Dispatcher.java create mode 100644 src/main/java/com/sourcegraph/webhook/EventSerializer.java create mode 100644 src/main/java/com/sourcegraph/webhook/WebhookListener.java create mode 100644 src/main/java/com/sourcegraph/webhook/WebhookRouter.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/EventEntity.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/Webhook.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/WebhookException.java create mode 100644 src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java diff --git a/pom.xml b/pom.xml index 37d2e3f..290f69b 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,11 @@ + + javax.inject + javax.inject + 1 + com.atlassian.bitbucket.server bitbucket-api @@ -62,6 +67,12 @@ provided 2.0 + + com.atlassian.activeobjects + activeobjects-plugin + 1.2.3 + provided + com.atlassian.plugin atlassian-spring-scanner-annotation @@ -90,7 +101,11 @@ sal-api provided - + + com.atlassian.bitbucket.server + bitbucket-spi + provided + @@ -152,7 +167,7 @@ atlassian-spring-scanner-external-jar - false + true diff --git a/src/main/java/com/sourcegraph/webhook/Dispatcher.java b/src/main/java/com/sourcegraph/webhook/Dispatcher.java new file mode 100644 index 0000000..86577ba --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/Dispatcher.java @@ -0,0 +1,106 @@ +package com.sourcegraph.webhook; + +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.sal.api.net.Request; +import com.atlassian.sal.api.net.RequestFactory; +import com.atlassian.sal.api.net.Response; +import com.atlassian.sal.api.net.ResponseException; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.sourcegraph.webhook.registry.Webhook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.concurrent.ExecutorService; + +@Component +public class Dispatcher { + private static final Logger log = LoggerFactory.getLogger(Dispatcher.class); + private static final int RETRY_DELAY = 10 * 1000; + private static final int MAX_ATTEMPTS = 5; + @ComponentImport + private static ExecutorService executor; + @ComponentImport + private static RequestFactory requestFactory; + + @Autowired + public Dispatcher(ExecutorService executor, RequestFactory requestFactory) { + Dispatcher.executor = executor; + Dispatcher.requestFactory = requestFactory; + } + + private static String sign(String secret, String data) throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance("HmacSHA1"); + SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1"); + mac.init(secretKeySpec); + return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8))); + } + + private static Request createRequest(Webhook hook, EventSerializer serializer) { + Request request = requestFactory.createRequest(Request.MethodType.POST, hook.endpoint); + request.setHeader("X-Event-Key", serializer.getName()); + request.setHeader("X-Hook-ID", String.valueOf(hook.id)); + request.setHeader("X-Hook-Name", hook.name); + + JsonObject payload = serializer.serialize(); + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + String json = gson.toJson(payload); + request.setRequestBody(json); + + try { + request.setHeader("X-Hub-Signature", sign(hook.secret, json)); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + log.error(e.toString()); + return null; + } + + return request; + } + + public static void dispatch(Webhook hook, EventSerializer serializer) { + executor.submit(() -> { + Request request = createRequest(hook, serializer); + if (request == null) { + return; + } + + int attempt = 0; + while (true) { + request.setHeader("X-Attempt-Number", String.valueOf(attempt)); + try { + Response response = (Response) request.executeAndReturn((resp) -> resp); + if (response.isSuccessful()) { + log.debug("Dispatching webhook (" + serializer.getName() + ") data to URL: [" + hook.endpoint + "] succeeded."); + break; + } + } catch (ResponseException e) { + log.debug("Dispatching webhook data (" + serializer.getName() + ") to URL: [" + hook.endpoint + "] failed with error:\n" + e); + } + attempt++; + + if (attempt == MAX_ATTEMPTS) { + log.warn("Dispatching webhook data (" + serializer.getName() + ") to URL: [" + hook.endpoint + "] failed after " + attempt + " attempts.."); + break; + } + + try { + Thread.sleep(RETRY_DELAY); + } catch (InterruptedException e) { + log.debug("Dispatching webhook data (" + serializer.getName() + ") to URL: [" + hook.endpoint + "] was interrupted."); + break; + } + } + }); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/EventSerializer.java b/src/main/java/com/sourcegraph/webhook/EventSerializer.java new file mode 100644 index 0000000..71463de --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/EventSerializer.java @@ -0,0 +1,114 @@ +package com.sourcegraph.webhook; + +import com.atlassian.bitbucket.event.ApplicationEvent; +import com.atlassian.bitbucket.event.pull.*; +import com.atlassian.bitbucket.json.JsonRenderer; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.gson.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +@Component +public class EventSerializer { + private static final SimpleDateFormat RFC3339 = new SimpleDateFormat("yyyy-MM-dd'T'h:m:ssZZZZZ"); + private static Map, Adapter> adapters = new HashMap<>(); + private static JsonRenderer renderer; + + private ApplicationEvent event; + private String name; + private JsonObject payload; + + private static JsonElement render(Object o) { + HashMap options = new HashMap<>(); + String raw = renderer.render(o, options); + return raw == null ? null : new JsonParser().parse(raw); + } + + private static Adapter PullRequestUpdatedEventAdapter = (element, event) -> { + element.addProperty("previousTitle", event.getPreviousTitle()); + element.addProperty("previousDescription", event.getPreviousDescription()); + element.add("previousTarget", render(event.getPreviousToBranch())); + }; + + private static Adapter PullRequestReviewersUpdatedEventAdapter = (element, event) -> { + element.add("removedReviewers", render(event.getRemovedReviewers())); + element.add("addedReviewers", render(event.getAddedReviewers())); + }; + + private static Adapter PullRequestParticipantStatusUpdatedEventAdapter = (element, event) -> { + element.add("participant", render(event.getParticipant())); + element.addProperty("previousStatus", event.getPreviousStatus().name()); + }; + + private static Adapter PullRequestCommentEventAdapter = (element, event) -> { + element.add("comment", render(event.getComment())); + if (event.getParent() != null) { + element.addProperty("commentParentId", event.getParent().getId()); + } + }; + + private static Adapter PullRequestCommentEditedEventAdapter = (element, event) -> { + PullRequestCommentEventAdapter.apply(element, event); + element.addProperty("previousComment", event.getPreviousText()); + }; + + static { + adapters.put(PullRequestUpdatedEvent.class, PullRequestUpdatedEventAdapter); + adapters.put(PullRequestReviewersUpdatedEvent.class, PullRequestReviewersUpdatedEventAdapter); + adapters.put(PullRequestParticipantApprovedEvent.class, PullRequestParticipantStatusUpdatedEventAdapter); + adapters.put(PullRequestParticipantUnapprovedEvent.class, PullRequestParticipantStatusUpdatedEventAdapter); + adapters.put(PullRequestParticipantReviewedEvent.class, PullRequestParticipantStatusUpdatedEventAdapter); + adapters.put(PullRequestCommentAddedEvent.class, PullRequestCommentEventAdapter); + adapters.put(PullRequestCommentDeletedEvent.class, PullRequestCommentEventAdapter); + adapters.put(PullRequestCommentEditedEvent.class, PullRequestCommentEditedEventAdapter); + } + + @Autowired + public EventSerializer(@ComponentImport JsonRenderer renderer) { + EventSerializer.renderer = renderer; + } + + public EventSerializer(String name, ApplicationEvent event) { + payload = new JsonObject(); + this.name = name; + this.event = event; + } + + public JsonObject serialize() { + buildApplicationEvent(this.event); + if (event instanceof PullRequestEvent) { + buildPullRequestEvent((PullRequestEvent) event); + } + + Adapter adapter = adapters.get(event.getClass()); + if (adapter != null) { + adapter.apply(payload, event); + } + return payload; + } + + public String getName() { + return this.name; + } + + public ApplicationEvent getEvent() { + return this.event; + } + + private void buildApplicationEvent(ApplicationEvent event) { + payload.addProperty("date", RFC3339.format(event.getDate())); + payload.add("actor", render(event.getUser())); + } + + private void buildPullRequestEvent(PullRequestEvent event) { + payload.add("pullRequest", render(event.getPullRequest())); + } + + private interface Adapter { + void apply(JsonObject element, T event); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/WebhookListener.java b/src/main/java/com/sourcegraph/webhook/WebhookListener.java new file mode 100644 index 0000000..5507db9 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/WebhookListener.java @@ -0,0 +1,71 @@ +package com.sourcegraph.webhook; + +import com.atlassian.bitbucket.event.ApplicationEvent; +import com.atlassian.bitbucket.event.pull.*; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.event.api.AsynchronousPreferred; +import com.atlassian.event.api.EventListener; +import com.sourcegraph.webhook.registry.Webhook; +import com.sourcegraph.webhook.registry.WebhookRegistry; + +import javax.inject.Named; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@AsynchronousPreferred +@Named("WebhookListener") +public class WebhookListener { + private static Map, List> triggers = new HashMap<>(); + + private static void register(Class type, String key) { + // Enumerate all prefixes (or super/parent events) of event key. + // "pr:comment:added" -> ["pr", "pr:comment", "pr:comment:added"] + String[] split = key.split(":"); + List prefixes = new ArrayList<>(); + for (int i = 0, index = 0; i < split.length; i++) { + index += split[i].length(); + prefixes.add(key.substring(0, index)); + index++; + } + triggers.put(type, prefixes); + } + + static { + register(PullRequestReopenedEvent.class, "pr:opened"); + register(PullRequestOpenedEvent.class, "pr:opened"); + register(PullRequestUpdatedEvent.class, "pr:modified"); + register(PullRequestReviewersUpdatedEvent.class, "pr:reviewer:updated"); + register(PullRequestParticipantApprovedEvent.class, "pr:reviewer:approved"); + register(PullRequestParticipantUnapprovedEvent.class, "pr:reviewer:unapproved"); + register(PullRequestParticipantReviewedEvent.class, "pr:reviewer:needs_work"); + register(PullRequestMergeActivityEvent.class, "pr:merged"); + register(PullRequestDeclinedEvent.class, "pr:declined"); + register(PullRequestDeletedEvent.class, "pr:deleted"); + register(PullRequestCommentAddedEvent.class, "pr:comment:added"); + register(PullRequestCommentEditedEvent.class, "pr:comment:edited"); + register(PullRequestCommentDeletedEvent.class, "pr:comment:deleted"); + } + + @EventListener + public void onPullRequestEvent(PullRequestEvent event) { + handle(event, event.getPullRequest().getToRef().getRepository()); + } + + private void handle(ApplicationEvent event, Repository repository) { + List keys = triggers.get(event.getClass()); + if (keys == null || keys.isEmpty()) { + return; + } + + List hooks = WebhookRegistry.getWebhooks(keys, repository); + if (hooks.isEmpty()) { + return; + } + + String key = keys.get(keys.size() - 1); + EventSerializer serializer = new EventSerializer(key, event); + hooks.forEach(hook -> Dispatcher.dispatch(hook, serializer)); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/WebhookRouter.java b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java new file mode 100644 index 0000000..0c631d4 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/WebhookRouter.java @@ -0,0 +1,60 @@ +package com.sourcegraph.webhook; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import com.sourcegraph.webhook.registry.Webhook; +import com.sourcegraph.webhook.registry.WebhookException; +import com.sourcegraph.webhook.registry.WebhookRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +@Path("/webhook") +public class WebhookRouter { + private static final Logger log = LoggerFactory.getLogger(WebhookRouter.class); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response get() { + List hooks = WebhookRegistry.getWebhooks(); + String resp = new Gson().toJson(hooks); + return Response.ok(resp).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response put(String raw) throws WebhookException { + try { + Gson gson = new Gson(); + Webhook hook = gson.fromJson(raw, Webhook.class); + if (hook == null) { + throw new WebhookException(Response.Status.BAD_REQUEST, "Invalid JSON"); + } + log.info("Registering webhook: " + raw); + WebhookRegistry.register(hook); + } catch (JsonIOException e) { + throw new WebhookException(Response.Status.INTERNAL_SERVER_ERROR, ""); + } catch (JsonSyntaxException e) { + throw new WebhookException(Response.Status.BAD_REQUEST, "Invalid JSON"); + } + + return Response.ok().build(); + } + + @DELETE + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response delete(@FormParam("id") int id, @FormParam("name") String name) { + if (name != null) { + WebhookRegistry.deregister(name); + } else { + WebhookRegistry.deregister(id); + } + + return Response.noContent().build(); + } +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java new file mode 100644 index 0000000..dfd9d06 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/EventEntity.java @@ -0,0 +1,13 @@ +package com.sourcegraph.webhook.registry; + +import net.java.ao.Entity; + +public interface EventEntity extends Entity { + String getEvent(); + + void setEvent(String event); + + WebhookEntity getWebhook(); + + void setWebhook(WebhookEntity hook); +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/Webhook.java b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java new file mode 100644 index 0000000..9a70035 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/Webhook.java @@ -0,0 +1,32 @@ +package com.sourcegraph.webhook.registry; + +import com.google.gson.annotations.Expose; + +import java.util.Set; + +public class Webhook { + @Expose + public int id; + @Expose + public String name; + @Expose + public String scope; + @Expose + public String identifier; + @Expose + public Set events; + @Expose + public String endpoint; + @Expose(serialize = false) + public String secret; + + public Webhook(int id, String name, String scope, String identifier, Set events, String endpoint, String secret) { + this.id = id; + this.name = name; + this.scope = scope; + this.identifier = identifier; + this.events = events; + this.endpoint = endpoint; + this.secret = secret; + } +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java new file mode 100644 index 0000000..35cc63d --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookEntity.java @@ -0,0 +1,19 @@ +package com.sourcegraph.webhook.registry; + +import net.java.ao.Entity; +import net.java.ao.OneToMany; + +public interface WebhookEntity extends Entity { + String getName(); + + String getScope(); + + int getIdentifier(); + + @OneToMany + EventEntity[] getEvents(); + + String getEndpoint(); + + String getSecret(); +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java new file mode 100644 index 0000000..895e8ed --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookException.java @@ -0,0 +1,51 @@ +package com.sourcegraph.webhook.registry; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class WebhookException extends Exception implements ExceptionMapper { + private Response.StatusType status; + + public WebhookException() { + } + + public WebhookException(Response.StatusType status, String message) { + super(message); + this.status = status; + } + + public Response.StatusType getStatus() { + return status; + } + + @Override + public Response toResponse(WebhookException e) { + return Response.status(e.getStatus()).entity(e.getMessage()).build(); + } + + public enum Status implements Response.StatusType { + UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"); + + private final int code; + private final String reason; + + Status(int status, String reason) { + this.code = status; + this.reason = reason; + } + + public int getStatusCode() { + return this.code; + } + + public Response.Status.Family getFamily() { + return null; + } + + public String getReasonPhrase() { + return this.reason; + } + } +} diff --git a/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java new file mode 100644 index 0000000..4de29f4 --- /dev/null +++ b/src/main/java/com/sourcegraph/webhook/registry/WebhookRegistry.java @@ -0,0 +1,157 @@ +package com.sourcegraph.webhook.registry; + +import com.atlassian.activeobjects.external.ActiveObjects; +import com.atlassian.bitbucket.project.Project; +import com.atlassian.bitbucket.project.ProjectService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; +import net.java.ao.Query; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import java.util.*; + +@Component +public class WebhookRegistry { + @ComponentImport + private static ActiveObjects activeObjects; + @ComponentImport + private static ProjectService projects; + @ComponentImport + private static RepositoryService repositories; + + @Autowired + public WebhookRegistry(ActiveObjects ao, ProjectService projects, RepositoryService repositories) { + WebhookRegistry.activeObjects = ao; + WebhookRegistry.projects = projects; + WebhookRegistry.repositories = repositories; + } + + public static List getWebhooks() { + WebhookEntity[] entities = activeObjects.find(WebhookEntity.class); + return getWebhooksFromEntities(entities); + } + + public static List getWebhooks(List keys, Repository repository) { + String params = Joiner.on(", ").join(Collections.nCopies(keys.size(), "?")); + Iterable args = Iterables.concat(keys, Arrays.asList( + repository.getId(), + repository.getProject().getId() + )); + + String where = "event.EVENT in (" + params + ") " + + "AND (webhook.SCOPE = \'global\' " + + "OR (webhook.SCOPE = \'project\' AND webhook.IDENTIFIER = ?) " + + "OR (webhook.SCOPE = \'repository\' AND webhook.IDENTIFIER = ?))"; + + Query query = Query.select() + .alias(WebhookEntity.class, "webhook") + .alias(EventEntity.class, "event") + .join(EventEntity.class, "event.WEBHOOK_ID = webhook.ID") + .where(where, Iterables.toArray(args, Object.class)); + + WebhookEntity[] hooks = activeObjects.find(WebhookEntity.class, query); + return getWebhooksFromEntities(hooks); + } + + private static List getWebhooksFromEntities(WebhookEntity[] entities) { + List hooks = new ArrayList<>(entities.length); + + for (WebhookEntity ent : entities) { + Set events = new HashSet<>(); + for (EventEntity ev : ent.getEvents()) { + events.add(ev.getEvent()); + } + + String name = resolveName(ent.getScope(), ent.getIdentifier()); + hooks.add(new Webhook(ent.getID(), ent.getName(), ent.getScope(), name, events, ent.getEndpoint(), ent.getSecret())); + } + return hooks; + } + + public static void register(Webhook hook) throws WebhookException { + int identifier = resolveID(hook.scope, hook.identifier); + + activeObjects.executeInTransaction(() -> { + Map params = new HashMap<>(); + params.put("SCOPE", hook.scope); + params.put("NAME", hook.name); + params.put("IDENTIFIER", identifier); + params.put("ENDPOINT", hook.endpoint); + params.put("SECRET", hook.secret); + + WebhookEntity hookEntity = activeObjects.create(WebhookEntity.class, params); + hookEntity.save(); + + for (String event : hook.events) { + EventEntity eventEntity = activeObjects.create(EventEntity.class); + eventEntity.setEvent(event); + eventEntity.setWebhook(hookEntity); + eventEntity.save(); + } + + return hookEntity; + }); + } + + public static void deregister(int id) { + activeObjects.executeInTransaction(() -> { + activeObjects.deleteWithSQL(EventEntity.class, "WEBHOOK_ID = ?", id); + activeObjects.deleteWithSQL(WebhookEntity.class, "ID = ?", id); + return null; + }); + } + + public static void deregister(String name) { + activeObjects.executeInTransaction(() -> { + WebhookEntity[] hooks = activeObjects.find(WebhookEntity.class, "NAME = ?", name); + for (WebhookEntity hook : hooks) { + for (EventEntity event : hook.getEvents()) { + activeObjects.delete(event); + } + activeObjects.delete(hook); + } + return null; + }); + } + + private static int resolveID(String scope, String name) throws WebhookException { + switch (scope) { + case "repository": + String[] split = name.split("/"); + Repository repository = repositories.getBySlug(split[0], split[1]); + if (repository == null) { + throw new WebhookException(Response.Status.NOT_FOUND, "No such repository: " + name); + } + return repository.getId(); + case "project": + Project project = projects.getByName(name); + if (project == null) { + throw new WebhookException(Response.Status.NOT_FOUND, "No such project: " + name); + } + return project.getId(); + case "global": + return 0; + default: + throw new WebhookException(WebhookException.Status.UNPROCESSABLE_ENTITY, "Invalid scope: " + scope); + } + } + + private static String resolveName(String scope, int id) { + switch (scope) { + case "repository": + Repository repository = repositories.getById(id); + return repository == null ? "" : repository.getName(); + case "project": + Project project = projects.getById(id); + return project == null ? "" : project.getName(); + default: + return ""; + } + } +} + diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index facf0e4..55bfc31 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -7,6 +7,12 @@ images/pluginLogo.png + + + com.sourcegraph.webhook.registry.WebhookEntity + com.sourcegraph.webhook.registry.EventEntity + + com.atlassian.auiplugin:ajs @@ -33,8 +39,8 @@ /plugins/servlet/sourcegraph + - Provides REST resources for the Sourcegraph admin UI. + Provides REST resources for Sourcegraph. -