From ef41e087918c29b600c000db065a79f7d0e81815 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Thu, 21 Nov 2024 14:21:10 +0100 Subject: [PATCH 1/6] Enable project key validation --- .../resource/JiraWebHookListenerResource.java | 3 +- .../service/validation/ConfiguredProject.java | 4 +- .../ConfiguredProjectValidator.java | 24 ++++++------ .../validation/ConfiguredProjectsService.java | 38 +++++++++++++++++++ .../replicate/jira/SimpleProjectHookTest.java | 3 +- 5 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java diff --git a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java index 7ba31d9..a7ca033 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java @@ -2,6 +2,7 @@ import org.hibernate.infra.replicate.jira.service.jira.JiraService; import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookEvent; +import org.hibernate.infra.replicate.jira.service.validation.ConfiguredProject; import org.jboss.resteasy.reactive.RestPath; @@ -22,7 +23,7 @@ public class JiraWebHookListenerResource { @POST @Path("/{project}") @Consumes(MediaType.APPLICATION_JSON) - public String somethingHappened(@RestPath @NotNull /* @ConfiguredProject */ String project, + public String somethingHappenedUpstream(@RestPath @NotNull @ConfiguredProject String project, JiraWebHookEvent event) { Log.infof("Received a notification about %s project: %.200s...", project, event); jiraService.acknowledge(project, event); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProject.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProject.java index 77f506c..04e63cf 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProject.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProject.java @@ -20,7 +20,9 @@ @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) public @interface ConfiguredProject { - String message() default "The {project} project is not configured"; + String message() default "The ${project} project is not configured"; + + boolean upstream() default true; Class[] groups() default {}; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectValidator.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectValidator.java index 8301d8c..0cf9676 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectValidator.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectValidator.java @@ -1,25 +1,21 @@ package org.hibernate.infra.replicate.jira.service.validation; -import java.util.Locale; -import java.util.Set; - -import org.hibernate.infra.replicate.jira.JiraConfig; import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -// @ApplicationScoped public class ConfiguredProjectValidator implements ConstraintValidator { - private final Set projects; + @Inject + Instance configuredProjectsService; + private boolean upstream; - // @Inject - public ConfiguredProjectValidator() { - JiraConfig jiraConfig = null; - projects = Set.of(); - // jiraConfig.projects().keySet().stream().map( s -> s.toLowerCase( Locale.ROOT - // ) ).collect( Collectors.toSet() ); + @Override + public void initialize(ConfiguredProject constraintAnnotation) { + upstream = constraintAnnotation.upstream(); } @Override @@ -27,7 +23,9 @@ public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } - if (projects.contains(value.toLowerCase(Locale.ROOT))) { + if (upstream + ? configuredProjectsService.get().isUpstreamProject(value) + : configuredProjectsService.get().isDownstreamProject(value)) { return true; } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java new file mode 100644 index 0000000..e9f84fe --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java @@ -0,0 +1,38 @@ +package org.hibernate.infra.replicate.jira.service.validation; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.infra.replicate.jira.JiraConfig; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ConfiguredProjectsService { + + private final Set upstreamProjects; + private final Set downstreamProjects; + + public ConfiguredProjectsService(JiraConfig jiraConfig) { + Set down = new HashSet<>(); + Set up = new HashSet<>(); + for (JiraConfig.JiraProjectGroup group : jiraConfig.projectGroup().values()) { + for (JiraConfig.JiraProject project : group.projects().values()) { + up.add(project.originalProjectKey()); + down.add(project.projectKey()); + } + } + + upstreamProjects = Collections.unmodifiableSet(up); + downstreamProjects = Collections.unmodifiableSet(down); + } + + public boolean isUpstreamProject(String projectName) { + return upstreamProjects.contains(projectName); + } + + public boolean isDownstreamProject(String projectName) { + return downstreamProjects.contains(projectName); + } +} diff --git a/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java b/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java index 459b1c9..d432889 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java @@ -34,8 +34,7 @@ class SimpleProjectHookTest { @Test void unknown() { given().when().body(REQUEST_BODY).contentType(ContentType.JSON).post("api/jira/webhooks/NOTAPROJECTKEY").then() - .statusCode(500).body(containsString( - "Unable to determine handler context for project NOTAPROJECTKEY. Was it not configured")); + .statusCode(400).body(containsString("The NOTAPROJECTKEY project is not configured")); } @Test From b4e4089506aa561bd9a382c7c4b585b3e5c430ad Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Thu, 21 Nov 2024 14:49:44 +0100 Subject: [PATCH 2/6] Allow ignoring updates by specific users --- .../infra/replicate/jira/JiraConfig.java | 8 ++++++++ .../resource/JiraWebHookListenerResource.java | 5 +++-- .../service/jira/HandlerProjectContext.java | 4 ++++ .../jira/service/jira/JiraService.java | 17 +++++++++++++---- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java index cbd7480..304386f 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java @@ -366,6 +366,14 @@ interface UserValueMapping extends ValueMapping { * {@link #mapping() mapping}. */ Optional profileUrl(); + + /** + * It may be helpful in some cases to ignore webhook events triggered by some + * users. E.g. the sync user that applies updates upstream can be listed here to + * prevent infinite update loop. + */ + @WithDefault("not-a-user") + Set ignoredUpstreamUsers(); } /** diff --git a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java index a7ca033..8ab4c5e 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java @@ -12,6 +12,7 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; @Path("/jira/webhooks") @@ -24,9 +25,9 @@ public class JiraWebHookListenerResource { @Path("/{project}") @Consumes(MediaType.APPLICATION_JSON) public String somethingHappenedUpstream(@RestPath @NotNull @ConfiguredProject String project, - JiraWebHookEvent event) { + @QueryParam("triggeredByUser") String triggeredByUser, JiraWebHookEvent event) { Log.infof("Received a notification about %s project: %.200s...", project, event); - jiraService.acknowledge(project, event); + jiraService.acknowledge(project, event, triggeredByUser); return "ack"; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index 44fb9b0..72b4882 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -347,4 +347,8 @@ private static boolean versionNeedsUpdate(JiraVersion upstreamVersion, JiraVersi || upstreamVersion.released != downstreamVersion.released || !Objects.equals(upstreamVersion.releaseDate, downstreamVersion.releaseDate); } + + public boolean isUserIgnored(String triggeredByUser) { + return projectGroupContext.projectGroup().users().ignoredUpstreamUsers().contains(triggeredByUser); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java index 6e4d873..1150976 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java @@ -46,6 +46,8 @@ @ApplicationScoped public class JiraService { + private static final String SYSTEM_USER = "94KJcxFzgxZlXyTss4oR0rDNqtjwjhIiZLzYNx0Mwuc="; + private final ReportingConfig reportingConfig; private final Map contextPerProject; private final JiraConfig jiraConfig; @@ -297,8 +299,10 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { * {@link JiraConfig.JiraProjectGroup#projects()} * @param event * The body of the event posted by the webhook. + * @param triggeredByUser + * The ID of the Jira user that triggered the webhook event. */ - public void acknowledge(String project, JiraWebHookEvent event) { + public void acknowledge(String project, JiraWebHookEvent event, String triggeredByUser) { event.eventType().ifPresentOrElse(eventType -> { var context = contextPerProject.get(project); if (context == null) { @@ -309,6 +313,11 @@ public void acknowledge(String project, JiraWebHookEvent event) { throw new ConstraintViolationException("Project " + project + " is not configured.", Set.of()); } + if (context.isUserIgnored(triggeredByUser)) { + Log.infof("Event was triggered by %s user that is in the ignore list.", triggeredByUser); + return; + } + for (Runnable handler : eventType.handlers(reportingConfig, event, context)) { context.submitTask(handler); } @@ -381,7 +390,7 @@ private void triggerSyncEvent(JiraIssue jiraIssue, HandlerProjectContext context event.issue = issue; String projectKey = Objects.toString(jiraIssue.fields.project.properties().get("key")); - acknowledge(projectKey, event); + acknowledge(projectKey, event, SYSTEM_USER); // now sync comments: if (jiraIssue.fields.comment != null && jiraIssue.fields.comment.comments != null) { @@ -401,7 +410,7 @@ private void triggerSyncEvent(JiraIssue jiraIssue, HandlerProjectContext context event.issueLink = new JiraWebHookIssueLink(); event.issueLink.id = Long.parseLong(link.id); - acknowledge(projectKey, event); + acknowledge(projectKey, event, SYSTEM_USER); } } } @@ -413,7 +422,7 @@ private void triggerCommentSyncEvents(String projectKey, JiraWebHookIssue issue, event.comment.id = Long.parseLong(comment.id); event.issue = issue; event.webhookEvent = JiraWebhookEventType.COMMENT_UPDATED.getName(); - acknowledge(projectKey, event); + acknowledge(projectKey, event, SYSTEM_USER); } } } From 984c156e76d5c7a12508f5c534ec4172fa2049b2 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Thu, 21 Nov 2024 17:45:03 +0100 Subject: [PATCH 3/6] Add handling of downstream automation events --- .../infra/replicate/jira/JiraConfig.java | 19 +++- .../resource/JiraWebHookListenerResource.java | 13 ++- .../service/jira/HandlerProjectContext.java | 20 ++++ .../jira/HandlerProjectGroupContext.java | 49 +++++++++- .../jira/service/jira/JiraService.java | 24 +++++ .../jira/handler/JiraEventHandler.java | 2 +- .../action/JiraActionEventHandler.java | 49 ++++++++++ .../JiraAssigneeActionEventHandler.java | 41 ++++++++ .../jira/model/action/JiraActionEvent.java | 26 +++++ .../jira/model/hook/JiraActionEventType.java | 57 +++++++++++ .../service/jira/model/rest/JiraFields.java | 8 ++ .../service/jira/model/rest/JiraUser.java | 10 ++ .../service/jira/model/rest/JiraVersion.java | 2 +- .../validation/RequestSignatureFilter.java | 95 +++++++++++++------ .../RequestSignatureFilterTest.java | 11 ++- 15 files changed, 388 insertions(+), 38 deletions(-) create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java diff --git a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java index 304386f..463b036 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java @@ -192,6 +192,11 @@ interface JiraProject { * Allows enabling signature verification. */ WebHookSecurity security(); + + /** + * Allows enabling signature verification of downstream events. + */ + WebHookSecurity downstreamSecurity(); } interface WebHookSecurity { @@ -205,11 +210,20 @@ interface WebHookSecurity { @WithDefault("false") boolean enabled(); + @WithDefault("SIGNATURE") + Type type(); + /** - * The secret used to sing the web hook request body. + * Verification secret, e.g. the secret used to sing the web hook request body. + * Can also be just some token that we will compare. Depends on the security + * type. */ @WithDefault("not-a-secret") String secret(); + + enum Type { + SIGNATURE, TOKEN + } } interface Instance { @@ -374,6 +388,9 @@ interface UserValueMapping extends ValueMapping { */ @WithDefault("not-a-user") Set ignoredUpstreamUsers(); + + @WithDefault("not-a-user") + Set ignoredDownstreamUsers(); } /** diff --git a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java index 8ab4c5e..5b8aad6 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java @@ -1,6 +1,7 @@ package org.hibernate.infra.replicate.jira.resource; import org.hibernate.infra.replicate.jira.service.jira.JiraService; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookEvent; import org.hibernate.infra.replicate.jira.service.validation.ConfiguredProject; @@ -26,8 +27,18 @@ public class JiraWebHookListenerResource { @Consumes(MediaType.APPLICATION_JSON) public String somethingHappenedUpstream(@RestPath @NotNull @ConfiguredProject String project, @QueryParam("triggeredByUser") String triggeredByUser, JiraWebHookEvent event) { - Log.infof("Received a notification about %s project: %.200s...", project, event); + Log.tracef("Received a notification about %s project: %.200s...", project, event); jiraService.acknowledge(project, event, triggeredByUser); return "ack"; } + + @POST + @Path("/mirror/{project}") + @Consumes(MediaType.APPLICATION_JSON) + public String somethingHappenedDownstream(@RestPath @NotNull @ConfiguredProject(upstream = false) String project, + JiraActionEvent data) { + Log.tracef("Received a downstream notification about %s project: %s...", project, data); + jiraService.downstreamAcknowledge(project, data); + return "ack"; + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index 72b4882..e3ff728 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -181,6 +181,10 @@ public void startProcessingEvent() throws InterruptedException { projectGroupContext.startProcessingEvent(); } + public void startProcessingDownstreamEvent() throws InterruptedException { + projectGroupContext.startProcessingDownstreamEvent(); + } + public JiraUser notMappedAssignee() { return notMappedAssignee; } @@ -229,6 +233,14 @@ public void submitTask(Runnable runnable) { projectGroupContext.submitTask(runnable); } + public int pendingDownstreamEventsInCurrentContext() { + return projectGroupContext.pendingDownstreamEventsInCurrentContext(); + } + + public void submitDownstreamTask(Runnable runnable) { + projectGroupContext.submitDownstreamTask(runnable); + } + public Optional contextForProjectInSameGroup(String project) { if (!projectGroup().projects().containsKey(project)) { // different project group, don't bother @@ -351,4 +363,12 @@ private static boolean versionNeedsUpdate(JiraVersion upstreamVersion, JiraVersi public boolean isUserIgnored(String triggeredByUser) { return projectGroupContext.projectGroup().users().ignoredUpstreamUsers().contains(triggeredByUser); } + + public boolean isDownstreamUserIgnored(String triggeredByUser) { + return projectGroupContext.projectGroup().users().ignoredDownstreamUsers().contains(triggeredByUser); + } + + public String upstreamUser(String mappedValue) { + return projectGroupContext.upstreamUser(mappedValue); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java index 5c85030..2c1cf54 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java @@ -1,5 +1,8 @@ package org.hibernate.infra.replicate.jira.service.jira; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; @@ -17,9 +20,14 @@ public final class HandlerProjectGroupContext implements AutoCloseable { private final ExecutorService eventHandlingExecutor; private final Supplier workQueueSize; + + private final ExecutorService downstreamEventHandlingExecutor; + private final Supplier downstreamWorkQueueSize; private final ScheduledExecutorService rateLimiterExecutor = Executors.newScheduledThreadPool(1); private final Semaphore rateLimiter; + private final Semaphore downstreamRateLimiter; private final JiraConfig.JiraProjectGroup projectGroup; + private final Map invertedUsers; public HandlerProjectGroupContext(JiraConfig.JiraProjectGroup projectGroup) { this.projectGroup = projectGroup; @@ -28,21 +36,39 @@ public HandlerProjectGroupContext(JiraConfig.JiraProjectGroup projectGroup) { final int permits = processing.eventsPerTimeframe(); this.rateLimiter = new Semaphore(permits); + this.downstreamRateLimiter = new Semaphore(permits); rateLimiterExecutor.scheduleAtFixedRate(() -> { rateLimiter.drainPermits(); rateLimiter.release(permits); + downstreamRateLimiter.drainPermits(); + downstreamRateLimiter.release(permits); }, processing.timeframeInSeconds(), processing.timeframeInSeconds(), TimeUnit.SECONDS); LinkedBlockingDeque workQueue = new LinkedBlockingDeque<>(processing.queueSize()); workQueueSize = workQueue::size; eventHandlingExecutor = new ThreadPoolExecutor(processing.threads(), processing.threads(), 0L, TimeUnit.MILLISECONDS, workQueue); + + LinkedBlockingDeque downstreamWorkQueue = new LinkedBlockingDeque<>(processing.queueSize()); + downstreamWorkQueueSize = downstreamWorkQueue::size; + downstreamEventHandlingExecutor = new ThreadPoolExecutor(processing.threads(), processing.threads(), 0L, + TimeUnit.MILLISECONDS, downstreamWorkQueue); + + Map invertedUsers = new HashMap<>(); + for (var entry : projectGroup.users().mapping().entrySet()) { + invertedUsers.put(entry.getValue(), entry.getKey()); + } + this.invertedUsers = Collections.unmodifiableMap(invertedUsers); } public void startProcessingEvent() throws InterruptedException { rateLimiter.acquire(1); } + public void startProcessingDownstreamEvent() throws InterruptedException { + downstreamRateLimiter.acquire(1); + } + public JiraConfig.JiraProjectGroup projectGroup() { return projectGroup; } @@ -51,10 +77,18 @@ public int pendingEventsInCurrentContext() { return workQueueSize.get(); } + public int pendingDownstreamEventsInCurrentContext() { + return downstreamWorkQueueSize.get(); + } + public void submitTask(Runnable task) { eventHandlingExecutor.submit(task); } + public void submitDownstreamTask(Runnable task) { + downstreamEventHandlingExecutor.submit(task); + } + @Override public void close() { // when requesting to close the context we aren't expecting to process any other @@ -62,10 +96,15 @@ public void close() { if (!rateLimiterExecutor.isShutdown()) { rateLimiterExecutor.shutdownNow(); } - if (!eventHandlingExecutor.isShutdown()) { + closeEventExecutor(eventHandlingExecutor); + closeEventExecutor(downstreamEventHandlingExecutor); + } + + private static void closeEventExecutor(ExecutorService executor) { + if (!executor.isShutdown()) { try { - eventHandlingExecutor.shutdown(); - if (!eventHandlingExecutor.awaitTermination(2, TimeUnit.MINUTES)) { + executor.shutdown(); + if (!executor.awaitTermination(2, TimeUnit.MINUTES)) { Log.warnf("Not all events were processed before the shutdown"); } } catch (InterruptedException e) { @@ -73,4 +112,8 @@ public void close() { } } } + + public String upstreamUser(String mappedValue) { + return invertedUsers.get(mappedValue); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java index 1150976..aa3ff68 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java @@ -18,6 +18,7 @@ import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueDeleteEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueSimpleUpsertEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueTransitionOnlyEventHandler; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookEvent; import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookIssue; import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookIssueLink; @@ -324,6 +325,29 @@ public void acknowledge(String project, JiraWebHookEvent event, String triggered }, () -> Log.infof("Event type %s is not supported and cannot be handled.", event.webhookEvent)); } + public void downstreamAcknowledge(String project, JiraActionEvent event) { + event.eventType().ifPresentOrElse(eventType -> { + var context = contextPerProject.get(project); + if (context == null) { + FailureCollector failureCollector = FailureCollector.collector(reportingConfig); + failureCollector.critical("Unable to determine handler context for project %s. Was it not configured ?" + .formatted(project)); + failureCollector.close(); + throw new ConstraintViolationException("Project " + project + " is not configured.", Set.of()); + } + + if (context.isDownstreamUserIgnored(event.triggeredByUser)) { + Log.infof("Event was triggered by %s user that is in the ignore list.", event.triggeredByUser); + return; + } + + for (Runnable handler : eventType.handlers(reportingConfig, event, context)) { + context.submitDownstreamTask(handler); + } + }, () -> Log.infof("Event type %s is not supported and cannot be handled.", event.event)); + + } + public void syncLastUpdated(String projectGroup) { try (FailureCollector failureCollector = FailureCollector.collector(reportingConfig)) { Log.infof("Starting scheduled sync of issues for the project group %s", projectGroup); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java index 23280eb..3ba8536 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java @@ -198,7 +198,7 @@ public final void run() { context.startProcessingEvent(); doRun(); } catch (RuntimeException e) { - failureCollector.critical("Failed to handled the event: %s".formatted(this), e); + failureCollector.critical("Failed to handle the event: %s".formatted(this), e); } catch (InterruptedException e) { failureCollector.critical("Interrupted while waiting in the queue", e); Thread.currentThread().interrupt(); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java new file mode 100644 index 0000000..7834e43 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java @@ -0,0 +1,49 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler.action; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +import io.quarkus.logging.Log; + +public abstract class JiraActionEventHandler implements Runnable { + + protected final JiraActionEvent event; + protected final FailureCollector failureCollector; + protected final HandlerProjectContext context; + + protected JiraActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + JiraActionEvent event) { + this.event = event; + this.failureCollector = FailureCollector.collector(reportingConfig); + this.context = context; + } + + @Override + public final void run() { + try { + context.startProcessingDownstreamEvent(); + doRun(); + } catch (RuntimeException e) { + failureCollector.critical("Failed to handle the event: %s".formatted(this), e); + } catch (InterruptedException e) { + failureCollector.critical("Interrupted while waiting in the queue", e); + Thread.currentThread().interrupt(); + } finally { + failureCollector.close(); + Log.infof("Finished processing %s. Pending events in %s to process: %s", this.toString(), + context.projectGroupName(), context.pendingDownstreamEventsInCurrentContext()); + } + } + + protected String toSourceKey(String key) { + return "%s-%d".formatted(context.project().originalProjectKey(), JiraIssue.keyToLong(key)); + } + + protected abstract void doRun(); + + public abstract String toString(); + +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java new file mode 100644 index 0000000..ee64b6c --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java @@ -0,0 +1,41 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler.action; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraFields; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +public class JiraAssigneeActionEventHandler extends JiraActionEventHandler { + + public JiraAssigneeActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + JiraActionEvent event) { + super(reportingConfig, context, event); + } + + @Override + protected void doRun() { + JiraIssue issue = context.destinationJiraClient().getIssue(event.key); + + JiraIssue updated = new JiraIssue(); + updated.fields = JiraFields.empty(); + if (issue.fields.assignee != null) { + String accountId = context.upstreamUser( + issue.fields.assignee.mappedIdentifier(context.projectGroup().users().mappedPropertyName())); + + if (accountId != null) { + updated.fields.assignee = new JiraUser(accountId); + + } + } else { + updated.fields.assignee = new JiraUser("-1"); + } + context.sourceJiraClient().update(toSourceKey(event.key), updated); + } + + @Override + public String toString() { + return "JiraAssigneeActionEventHandler[" + "event=" + event + ", project=" + context.projectName() + ']'; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java new file mode 100644 index 0000000..3ce87c4 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java @@ -0,0 +1,26 @@ +package org.hibernate.infra.replicate.jira.service.jira.model.action; + +import java.util.Optional; + +import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; +import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraActionEventType; + +public class JiraActionEvent extends JiraBaseObject { + public String id; + public String key; + public String event; + public String assignee; + public String status; + + public String triggeredByUser; + + public Optional eventType() { + return JiraActionEventType.of(event); + } + + @Override + public String toString() { + return "JiraActionEvent{" + "id='" + id + '\'' + ", key='" + key + '\'' + ", event='" + event + '\'' + + ", assignee='" + assignee + '\'' + ", status='" + status + '\'' + '}'; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java new file mode 100644 index 0000000..3162eba --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java @@ -0,0 +1,57 @@ +package org.hibernate.infra.replicate.jira.service.jira.model.hook; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.handler.action.JiraAssigneeActionEventHandler; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +public enum JiraActionEventType { + ISSUE_ASSIGNED("jira:issue_assigned") { + @Override + public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, + HandlerProjectContext context) { + if (event.assignee == null || event.key == null) { + throw new IllegalStateException( + "Trying to handle an issue event but issue id is null: %s".formatted(event)); + } + return List.of(new JiraAssigneeActionEventHandler(reportingConfig, context, event)); + } + }, + ISSUE_TRANSITIONED("jira:issue_transitioned") { + @Override + public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, + HandlerProjectContext context) { + throw new UnsupportedOperationException("jira:issue_transitioned not supported yet"); + } + }; + + private final String name; + + JiraActionEventType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Optional of(String webhookEvent) { + if (webhookEvent == null) { + return Optional.empty(); + } + for (JiraActionEventType value : values()) { + if (value.name.equals(webhookEvent.toLowerCase(Locale.ROOT))) { + return Optional.of(value); + } + } + return Optional.empty(); + } + + public abstract Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, + HandlerProjectContext context); +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java index 53e77c5..844e474 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java @@ -26,6 +26,14 @@ public class JiraFields extends JiraBaseObject { public ZonedDateTime created; public ZonedDateTime updated; + public static JiraFields empty() { + JiraFields fields = new JiraFields(); + fields.priority = null; + fields.issuetype = null; + fields.project = null; + return fields; + } + @Override public String toString() { return "JiraFields{" + "summary='" + summary + '\'' + ", description=" + description + ", priority=" + priority diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraUser.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraUser.java index 33b2805..62bba01 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraUser.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraUser.java @@ -1,5 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.model.rest; +import java.util.Objects; + import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; public class JiraUser extends JiraBaseObject { @@ -22,6 +24,14 @@ public JiraUser(String propertyName, String value) { } } + public String mappedIdentifier(String propertyName) { + if ("accountId".equals(propertyName)) { + return this.accountId; + } else { + return Objects.toString(properties().get(propertyName), null); + } + } + public static JiraUser unassigned(String propertyName) { // { // "name": "-1" diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java index 1b0d71c..eec8f16 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraVersion.java @@ -54,7 +54,7 @@ public static Optional findVersion(String versionId, List macs; + private static final Pattern PATH_UPSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/(.+)"); + private static final Pattern PATH_DOWNSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/downstream/(.+)"); + private static final BiPredicate ALLOW_ALL = (a, b) -> true; + + private final Map> upstreamChecks; + private final Map> downstreamChecks; @Inject public RequestSignatureFilter(JiraConfig jiraConfig) { - Map macs = new HashMap<>(); + Map> up = new HashMap<>(); + Map> down = new HashMap<>(); for (JiraConfig.JiraProjectGroup group : jiraConfig.projectGroup().values()) { for (var entry : group.projects().entrySet()) { - JiraConfig.WebHookSecurity security = entry.getValue().security(); - if (security.enabled()) { - macs.put(entry.getKey(), fromSecret(security.secret())); - } + up.put(entry.getKey(), check(entry.getValue().security())); + down.put(entry.getKey(), check(entry.getValue().downstreamSecurity())); } } - this.macs = Collections.unmodifiableMap(macs); + this.upstreamChecks = Collections.unmodifiableMap(up); + this.downstreamChecks = Collections.unmodifiableMap(down); + } + + private BiPredicate check(JiraConfig.WebHookSecurity security) { + if (security.enabled()) { + switch (security.type()) { + case TOKEN -> { + String secret = security.secret(); + return (header, body) -> secret.equals(header); + } + case SIGNATURE -> { + Mac mac = fromSecret(security.secret()); + return (header, body) -> header.equals(signBytes(mac, body)); + } + default -> throw new IllegalArgumentException("Unsupported security type: " + security.type()); + } + } else { + return null; + } } @WithFormRead @ServerRequestFilter public Response checkSignature(ContainerRequestContext requestContext) throws IOException { String path = requestContext.getUriInfo().getPath(); - Matcher matcher = PATH_WEBHOOK_PATTERN.matcher(path); - if ("POST".equals(requestContext.getMethod()) && matcher.matches()) { - String project = matcher.group(1); - Mac mac = macs.get(project); - if (mac == null) { - // means security is not enabled for this project... - return null; + Matcher downstream = PATH_DOWNSTREAM_WEBHOOK_PATTERN.matcher(path); + Matcher upstream = PATH_UPSTREAM_WEBHOOK_PATTERN.matcher(path); + if ("POST".equals(requestContext.getMethod())) { + BiPredicate check = null; + if (downstream.matches()) { + // for downstream automated actions we just send something in the header that we + // compare here:3 + String project = downstream.group(1); + check = downstreamChecks.get(project); + } else if (upstream.matches()) { + String project = upstream.group(1); + check = upstreamChecks.get(project); } + if (check != null) { + String signature = requestContext.getHeaderString("x-hub-signature"); - String signature = requestContext.getHeaderString("x-hub-signature"); - - if (signature == null || !requestContext.hasEntity()) { - Log.warnf("Rejecting a web hook event because of the missing signature. Posted to %s", path); - return Response.status(401).entity("Invalid request. Missing x-hub-signature header.").build(); - } - try (InputStream entityStream = requestContext.getEntityStream()) { - byte[] payload = entityStream.readAllBytes(); + if (signature == null || !requestContext.hasEntity()) { + Log.warnf("Rejecting a web hook event because of the missing signature. Posted to %s", path); + return Response.status(401).entity("Invalid request. Missing x-hub-signature header.").build(); + } - final String calculatedSignature = sign(mac, payload); - if (!calculatedSignature.equals(signature)) { - Log.warnf("Rejecting a web hook event because of the signature mismatch. Posted to %s", path); + if (check(requestContext, check, signature, path)) { return Response.status(401).entity("Signatures do not match.").build(); } - requestContext.setEntityStream(new ByteArrayInputStream(payload)); } } return null; } + private static boolean check(ContainerRequestContext requestContext, BiPredicate check, + String signature, String path) throws IOException { + try (InputStream entityStream = requestContext.getEntityStream()) { + byte[] payload = entityStream.readAllBytes(); + + if (!check.test(signature, payload)) { + Log.warnf("Rejecting a web hook event because of the signature mismatch. Posted to %s", path); + return true; + } + requestContext.setEntityStream(new ByteArrayInputStream(payload)); + } + return false; + } + public static String sign(String secret, String payload) { - return sign(fromSecret(secret), payload.getBytes(StandardCharsets.UTF_8)); + return signBytes(fromSecret(secret), payload.getBytes(StandardCharsets.UTF_8)); } - public static String sign(Mac mac, byte[] payload) { + public static String signBytes(Mac mac, byte[] payload) { final byte[] digest = mac.doFinal(payload); final HexFormat hex = HexFormat.of(); return "sha256=" + hex.formatHex(digest); diff --git a/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java b/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java index db86c2d..95f4af1 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java @@ -69,7 +69,7 @@ private static RequestSignatureFilter createFilter(boolean enabled, String secre throws NoSuchAlgorithmException, InvalidKeyException { JiraConfig.JiraProjectGroup group = Mockito.mock(JiraConfig.JiraProjectGroup.class); JiraConfig.JiraProject project = Mockito.mock(JiraConfig.JiraProject.class); - Mockito.when(project.security()).thenReturn(new JiraConfig.WebHookSecurity() { + JiraConfig.WebHookSecurity value = new JiraConfig.WebHookSecurity() { @Override public boolean enabled() { return enabled; @@ -79,7 +79,14 @@ public boolean enabled() { public String secret() { return secret; } - }); + + @Override + public Type type() { + return Type.SIGNATURE; + } + }; + Mockito.when(project.security()).thenReturn(value); + Mockito.when(project.downstreamSecurity()).thenReturn(value); Mockito.when(group.projects()).thenReturn(Map.of("PROJECT_KEY", project)); From da35aa0affa16d45395c419c36875849a5153f65 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Wed, 4 Dec 2024 09:45:49 +0100 Subject: [PATCH 4/6] Add sync of the `Affects versions` --- .../jira/handler/JiraIssueAbstractEventHandler.java | 10 ++++++++++ .../jira/service/jira/model/rest/JiraFields.java | 1 + 2 files changed, 11 insertions(+) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java index 9a560e4..df700ae 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java @@ -174,6 +174,16 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss } } + if (sourceIssue.fields.versions != null) { + destinationIssue.fields.versions = new ArrayList<>(); + for (JiraVersion version : sourceIssue.fields.versions) { + JiraVersion downstream = context.fixVersion(version); + if (downstream != null) { + destinationIssue.fields.versions.add(downstream); + } + } + } + return destinationIssue; } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java index 844e474..3834c0f 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java @@ -18,6 +18,7 @@ public class JiraFields extends JiraBaseObject { public JiraUser assignee; public JiraUser reporter; public List fixVersions; + public List versions; // this is actually `Affects versions` // NOTE: this one is for "read-only" purposes, to create links a different API // has to be used public List issuelinks; From 5abf994836ccdd3cab57110906ed49ce1ac49e5c Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Wed, 4 Dec 2024 11:58:11 +0100 Subject: [PATCH 5/6] Add handling of more automation events --- .../resource/JiraWebHookListenerResource.java | 2 +- .../service/jira/HandlerProjectContext.java | 4 ++ .../jira/HandlerProjectGroupContext.java | 19 ++++-- .../jira/service/jira/JiraService.java | 2 +- .../jira/handler/JiraEventHandler.java | 23 -------- .../JiraIssueAbstractEventHandler.java | 5 +- .../handler/JiraStaticFieldMappingCache.java | 10 ++++ ...JiraAbstractVersionActionEventHandler.java | 54 +++++++++++++++++ .../JiraAffectsVersionActionEventHandler.java | 26 +++++++++ .../JiraFixVersionActionEventHandler.java | 26 +++++++++ .../JiraTransitionActionEventHandler.java | 58 +++++++++++++++++++ .../jira/model/action/JiraActionEvent.java | 4 +- .../jira/model/hook/JiraActionEventType.java | 23 +++++++- .../service/jira/model/rest/JiraFields.java | 4 ++ .../jira/model/rest/JiraTransitions.java | 25 ++++++++ .../validation/RequestSignatureFilter.java | 2 +- 16 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java diff --git a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java index 5b8aad6..6f4ddeb 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java @@ -35,7 +35,7 @@ public String somethingHappenedUpstream(@RestPath @NotNull @ConfiguredProject St @POST @Path("/mirror/{project}") @Consumes(MediaType.APPLICATION_JSON) - public String somethingHappenedDownstream(@RestPath @NotNull @ConfiguredProject(upstream = false) String project, + public String somethingHappenedDownstream(@RestPath @NotNull @ConfiguredProject String project, JiraActionEvent data) { Log.tracef("Received a downstream notification about %s project: %s...", project, data); jiraService.downstreamAcknowledge(project, data); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index e3ff728..b3001ba 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -371,4 +371,8 @@ public boolean isDownstreamUserIgnored(String triggeredByUser) { public String upstreamUser(String mappedValue) { return projectGroupContext.upstreamUser(mappedValue); } + + public String upstreamStatus(String mappedValue) { + return projectGroupContext.upstreamStatus(mappedValue); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java index 2c1cf54..bb8cfee 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java @@ -28,6 +28,7 @@ public final class HandlerProjectGroupContext implements AutoCloseable { private final Semaphore downstreamRateLimiter; private final JiraConfig.JiraProjectGroup projectGroup; private final Map invertedUsers; + private final Map invertedStatuses; public HandlerProjectGroupContext(JiraConfig.JiraProjectGroup projectGroup) { this.projectGroup = projectGroup; @@ -54,11 +55,16 @@ public HandlerProjectGroupContext(JiraConfig.JiraProjectGroup projectGroup) { downstreamEventHandlingExecutor = new ThreadPoolExecutor(processing.threads(), processing.threads(), 0L, TimeUnit.MILLISECONDS, downstreamWorkQueue); - Map invertedUsers = new HashMap<>(); - for (var entry : projectGroup.users().mapping().entrySet()) { - invertedUsers.put(entry.getValue(), entry.getKey()); + this.invertedUsers = invert(projectGroup.users().mapping()); + this.invertedStatuses = invert(projectGroup.statuses().mapping()); + } + + private static Map invert(Map map) { + Map result = new HashMap<>(); + for (var entry : map.entrySet()) { + result.put(entry.getValue(), entry.getKey()); } - this.invertedUsers = Collections.unmodifiableMap(invertedUsers); + return Collections.unmodifiableMap(result); } public void startProcessingEvent() throws InterruptedException { @@ -116,4 +122,9 @@ private static void closeEventExecutor(ExecutorService executor) { public String upstreamUser(String mappedValue) { return invertedUsers.get(mappedValue); } + + public String upstreamStatus(String mappedValue) { + return invertedStatuses.get(mappedValue); + } + } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java index aa3ff68..604bc10 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java @@ -315,7 +315,7 @@ public void acknowledge(String project, JiraWebHookEvent event, String triggered } if (context.isUserIgnored(triggeredByUser)) { - Log.infof("Event was triggered by %s user that is in the ignore list.", triggeredByUser); + Log.infof("Event was triggered by %s user that is in the ignore list: %.200s", triggeredByUser, event); return; } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java index 3ba8536..939c46f 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java @@ -1,7 +1,6 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; import java.net.URI; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -13,10 +12,8 @@ import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComment; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; -import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTextContent; -import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -101,26 +98,6 @@ protected Optional statusToTransition(String from, String to, Supplier%s".formatted(from, to), tk -> transitionFinder.get().orElse(null))); } - protected Optional findRequiredTransitionId(String downstreamStatus, JiraIssue destIssue) { - if (downstreamStatus != null) { - List jiraTransitions = null; - try { - JiraTransitions transitions = context.destinationJiraClient().availableTransitions(destIssue.key); - jiraTransitions = transitions.transitions; - } catch (Exception e) { - failureCollector.warning("Failed to find a transition for %s".formatted(destIssue.key), e); - jiraTransitions = Collections.emptyList(); - } - for (JiraIssueTransition transition : jiraTransitions) { - if (transition.to != null && downstreamStatus.equalsIgnoreCase(transition.to.name)) { - return Optional.of(transition.id); - } - } - } - - return Optional.empty(); - } - protected Optional linkType(String sourceId) { JiraConfig.ValueMapping mappedValues = context.projectGroup().issueLinkTypes(); return Optional.ofNullable(JiraStaticFieldMappingCache.linkType(context.projectGroupName(), sourceId, pk -> { diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java index df700ae..7e4174f 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java @@ -18,6 +18,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraRemoteLink; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -231,7 +232,9 @@ protected Optional prepareTransition(JiraSimpleObject sourceStat protected Optional prepareTransition(String downstreamStatus, JiraIssue destIssue) { return statusToTransition(destIssue.fields.status.name, downstreamStatus, - () -> findRequiredTransitionId(downstreamStatus, destIssue)).map(JiraTransition::new); + () -> JiraTransitions.findRequiredTransitionId(context.destinationJiraClient(), failureCollector, + downstreamStatus, destIssue)) + .map(JiraTransition::new); } protected Optional prepareParentLink(String destinationKey, JiraIssue sourceIssue) { diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java index c2929be..4c4af84 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java @@ -10,6 +10,7 @@ public class JiraStaticFieldMappingCache { private static final Map> priorities = new ConcurrentHashMap<>(); private static final Map> issueType = new ConcurrentHashMap<>(); private static final Map> status = new ConcurrentHashMap<>(); + private static final Map> statusUpstream = new ConcurrentHashMap<>(); private static final Map> linkType = new ConcurrentHashMap<>(); private static final Map> user = new ConcurrentHashMap<>(); @@ -24,6 +25,15 @@ public static String issueType(String projectGroup, String sourceId, } public static String status(String projectGroup, String transitionKey, Function onMissing) { + return status(status, projectGroup, transitionKey, onMissing); + } + + public static String statusUpstream(String projectGroup, String transitionKey, Function onMissing) { + return status(statusUpstream, projectGroup, transitionKey, onMissing); + } + + private static String status(Map> status, String projectGroup, String transitionKey, + Function onMissing) { Map groupStatuses = status.computeIfAbsent(projectGroup, pg -> new ConcurrentHashMap<>()); String id = groupStatuses.get(transitionKey); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java new file mode 100644 index 0000000..bdb461e --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java @@ -0,0 +1,54 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler.action; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraFields; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +abstract class JiraAbstractVersionActionEventHandler extends JiraActionEventHandler { + + public JiraAbstractVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + JiraActionEvent event) { + super(reportingConfig, context, event); + } + + @Override + protected void doRun() { + JiraIssue issue = context.destinationJiraClient().getIssue(event.key); + + JiraIssue updated = new JiraIssue(); + updated.fields = JiraFields.empty(); + + List versionList = versionList(issue); + + List versions; + if (versionList != null) { + versions = new ArrayList<>(versionList.size()); + for (JiraVersion ver : versionList) { + JiraVersion version = new JiraVersion(); + version.name = ver.name; + versions.add(version); + } + } else { + versions = List.of(); + } + + setVersionList(updated, versions); + + context.sourceJiraClient().update(toSourceKey(event.key), updated); + } + + protected abstract void setVersionList(JiraIssue issue, List versions); + + protected abstract List versionList(JiraIssue issue); + + @Override + public String toString() { + return this.getClass().getSimpleName() + "[" + "event=" + event + ", project=" + context.projectName() + ']'; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java new file mode 100644 index 0000000..32585c2 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java @@ -0,0 +1,26 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler.action; + +import java.util.List; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +public class JiraAffectsVersionActionEventHandler extends JiraAbstractVersionActionEventHandler { + + public JiraAffectsVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + JiraActionEvent event) { + super(reportingConfig, context, event); + } + + protected void setVersionList(JiraIssue issue, List versions) { + issue.fields.versions = versions; + } + + protected List versionList(JiraIssue issue) { + return issue.fields.versions; + } + +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java new file mode 100644 index 0000000..7eed14c --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java @@ -0,0 +1,26 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler.action; + +import java.util.List; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +public class JiraFixVersionActionEventHandler extends JiraAbstractVersionActionEventHandler { + + public JiraFixVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + JiraActionEvent event) { + super(reportingConfig, context, event); + } + + protected void setVersionList(JiraIssue issue, List versions) { + issue.fields.fixVersions = versions; + } + + protected List versionList(JiraIssue issue) { + return issue.fields.fixVersions; + } + +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java new file mode 100644 index 0000000..c171f45 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java @@ -0,0 +1,58 @@ +package org.hibernate.infra.replicate.jira.service.jira.handler.action; + +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.handler.JiraStaticFieldMappingCache; +import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; +import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; + +public class JiraTransitionActionEventHandler extends JiraActionEventHandler { + + public JiraTransitionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + JiraActionEvent event) { + super(reportingConfig, context, event); + } + + @Override + protected void doRun() { + String sourceKey = toSourceKey(event.key); + JiraIssue issue = context.destinationJiraClient().getIssue(event.key); + JiraIssue sourceIssue = context.sourceJiraClient().getIssue(sourceKey); + + String statusDownstream = issue.fields.status.name.toLowerCase(Locale.ROOT); + String statusCurrent = sourceIssue.fields.status.name.toLowerCase(Locale.ROOT); + + if (context.projectGroup().statuses().ignoreTransitionCondition().getOrDefault(statusCurrent, Set.of()) + .contains(statusDownstream)) { + return; + } + + String statusNew = context.upstreamStatus(statusDownstream); + + prepareTransition(statusNew, sourceIssue) + .ifPresent(jiraTransition -> context.sourceJiraClient().transition(sourceKey, jiraTransition)); + } + + protected Optional prepareTransition(String upstreamStatus, JiraIssue issue) { + return statusToTransition(issue.fields.status.name, upstreamStatus, () -> JiraTransitions + .findRequiredTransitionId(context.sourceJiraClient(), failureCollector, upstreamStatus, issue)) + .map(JiraTransition::new); + } + + protected Optional statusToTransition(String from, String to, Supplier> transitionFinder) { + return Optional.ofNullable(JiraStaticFieldMappingCache.statusUpstream(context.projectGroupName(), + "%s->%s".formatted(from, to), tk -> transitionFinder.get().orElse(null))); + } + + @Override + public String toString() { + return "JiraAssigneeActionEventHandler[" + "event=" + event + ", project=" + context.projectName() + ']'; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java index 3ce87c4..9d3c1ca 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/action/JiraActionEvent.java @@ -10,7 +10,7 @@ public class JiraActionEvent extends JiraBaseObject { public String key; public String event; public String assignee; - public String status; + public String value; public String triggeredByUser; @@ -21,6 +21,6 @@ public Optional eventType() { @Override public String toString() { return "JiraActionEvent{" + "id='" + id + '\'' + ", key='" + key + '\'' + ", event='" + event + '\'' - + ", assignee='" + assignee + '\'' + ", status='" + status + '\'' + '}'; + + ", assignee='" + assignee + '\'' + ", value='" + value + '\'' + '}'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java index 3162eba..8c025b3 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraActionEventType.java @@ -6,12 +6,15 @@ import java.util.Optional; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.handler.action.JiraAffectsVersionActionEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.action.JiraAssigneeActionEventHandler; +import org.hibernate.infra.replicate.jira.service.jira.handler.action.JiraFixVersionActionEventHandler; +import org.hibernate.infra.replicate.jira.service.jira.handler.action.JiraTransitionActionEventHandler; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; public enum JiraActionEventType { - ISSUE_ASSIGNED("jira:issue_assigned") { + ISSUE_ASSIGNED("jira:issue_update_assignee") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, HandlerProjectContext context) { @@ -22,11 +25,25 @@ public Collection handlers(ReportingConfig reportingConfig, JiraAction return List.of(new JiraAssigneeActionEventHandler(reportingConfig, context, event)); } }, - ISSUE_TRANSITIONED("jira:issue_transitioned") { + ISSUE_TRANSITIONED("jira:issue_update_status") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, HandlerProjectContext context) { - throw new UnsupportedOperationException("jira:issue_transitioned not supported yet"); + return List.of(new JiraTransitionActionEventHandler(reportingConfig, context, event)); + } + }, + FIX_VERSION_CHANGED("jira:issue_update_fixversions") { + @Override + public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, + HandlerProjectContext context) { + return List.of(new JiraFixVersionActionEventHandler(reportingConfig, context, event)); + } + }, + AFFECTS_VERSION_CHANGED("jira:issue_update_versions") { + @Override + public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, + HandlerProjectContext context) { + return List.of(new JiraAffectsVersionActionEventHandler(reportingConfig, context, event)); } }; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java index 3834c0f..f2b9f35 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java @@ -5,6 +5,8 @@ import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; +import com.fasterxml.jackson.annotation.JsonInclude; + public class JiraFields extends JiraBaseObject { public String summary; @@ -17,7 +19,9 @@ public class JiraFields extends JiraBaseObject { public JiraUser assignee; public JiraUser reporter; + @JsonInclude(JsonInclude.Include.NON_NULL) public List fixVersions; + @JsonInclude(JsonInclude.Include.NON_NULL) public List versions; // this is actually `Affects versions` // NOTE: this one is for "read-only" purposes, to create links a different API // has to be used diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java index ce69522..b11d608 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java @@ -1,9 +1,34 @@ package org.hibernate.infra.replicate.jira.service.jira.model.rest; +import java.util.Collections; import java.util.List; +import java.util.Optional; +import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestClient; import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; +import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; public class JiraTransitions extends JiraBaseObject { public List transitions; + + public static Optional findRequiredTransitionId(JiraRestClient client, FailureCollector failureCollector, + String status, JiraIssue issue) { + if (status != null) { + List jiraTransitions = null; + try { + JiraTransitions transitions = client.availableTransitions(issue.key); + jiraTransitions = transitions.transitions; + } catch (Exception e) { + failureCollector.warning("Failed to find a transition for %s".formatted(issue.key), e); + jiraTransitions = Collections.emptyList(); + } + for (JiraIssueTransition transition : jiraTransitions) { + if (transition.to != null && status.equalsIgnoreCase(transition.to.name)) { + return Optional.of(transition.id); + } + } + } + + return Optional.empty(); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java index 4283cb6..05e4220 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java @@ -32,7 +32,7 @@ public class RequestSignatureFilter { private static final Pattern PATH_UPSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/(.+)"); - private static final Pattern PATH_DOWNSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/downstream/(.+)"); + private static final Pattern PATH_DOWNSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/mirror/(.+)"); private static final BiPredicate ALLOW_ALL = (a, b) -> true; private final Map> upstreamChecks; From 3ad99a21acf5d3e28d4e7f9e144d4580fd1d4436 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Wed, 4 Dec 2024 12:12:39 +0100 Subject: [PATCH 6/6] Comment out the `notifyUsers=false` query parameter since `notifyUsers=false` does not apply to all requests and we've disabled notifications downstream this query param is not sent anymore to allow automation updating upstream issues to work with a non-admin user. --- .../replicate/jira/service/jira/client/JiraRestClient.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java index 0ff2c63..080b648 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java @@ -41,7 +41,9 @@ * version (included), e.g.: */ // so that we do not spam with all notifications ... -@ClientQueryParam(name = "notifyUsers", value = "false") +// since `notifyUsers=false` does not apply to all requests and we've disabled notifications downstream +// this query param is not sent anymore to allow automation updating upstream issues to work with a non-admin user. +// @ClientQueryParam(name = "notifyUsers", value = "false") public interface JiraRestClient { @GET