Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn about duplicated events received from GitHub via Admin Monitor #388

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fc6e29c
create an admin monitor for warning about duplicate events from github
gbhat618 Jan 31, 2025
a8bcb47
update the list of interested events and minor changes in cleanup
gbhat618 Feb 3, 2025
3d9c96b
consider tests sending stapler currentRequest null
gbhat618 Feb 4, 2025
0a3820c
use mocks instead of putting null check in execution code
gbhat618 Feb 4, 2025
c8f5a01
remove the fixed comment
gbhat618 Feb 4, 2025
978b8b6
track the last seen duplicate for 24 hours
gbhat618 Feb 5, 2025
bfd9df8
Update src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/…
gbhat618 Feb 5, 2025
e77068d
remove few bits to further simplify
gbhat618 Feb 5, 2025
188be21
provide a way to extract last logged event via logging in duplicates …
gbhat618 Feb 6, 2025
89ef2bd
add a comment for why tracking the prev event inside the logger eval
gbhat618 Feb 6, 2025
e3d9f80
log message should be clear to the user
gbhat618 Feb 6, 2025
a5dd3bf
update the admin monitor test to include duplicate event logging logic
gbhat618 Feb 6, 2025
b5ab212
show the payload via hyperlink not logging
gbhat618 Feb 7, 2025
57f050f
mark the method with GET
gbhat618 Feb 7, 2025
c0fe850
use variable for shorter line
gbhat618 Feb 8, 2025
aa61458
Update src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplica…
gbhat618 Feb 11, 2025
f311a6b
Update src/main/resources/org/jenkinsci/plugins/github/Messages.prope…
gbhat618 Feb 11, 2025
3e75413
Update src/main/resources/org/jenkinsci/plugins/github/Messages.prope…
gbhat618 Feb 11, 2025
e8fbb20
Update src/test/java/org/jenkinsci/plugins/github/admin/GitHubDuplica…
gbhat618 Feb 11, 2025
39350c7
Update src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplica…
gbhat618 Feb 11, 2025
5577148
Update src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplica…
gbhat618 Feb 11, 2025
8473753
update for comments
gbhat618 Feb 11, 2025
e46168d
add a comment and fix the javadoc
gbhat618 Feb 11, 2025
626e368
Update src/main/java/org/jenkinsci/plugins/github/extension/GHSubscri…
gbhat618 Feb 11, 2025
c65fc31
use caffeine for cache instead of ConcurrentHashMap
gbhat618 Feb 11, 2025
8334bda
add a note about cache size
gbhat618 Feb 11, 2025
82c5ab6
move the duplicate event subscriber as an inner class into the github…
gbhat618 Feb 14, 2025
50108ef
Update src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplica…
gbhat618 Feb 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/main/java/com/cloudbees/jenkins/GitHubWebHook.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public class GitHubWebHook implements UnprotectedRootAction {
// headers used for testing the endpoint configuration
public static final String URL_VALIDATION_HEADER = "X-Jenkins-Validation";
public static final String X_INSTANCE_IDENTITY = "X-Instance-Identity";
/**
* X-GitHub-Delivery: A globally unique identifier (GUID) to identify the event.
* @see <a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads#delivery-headers">Delivery
* headers</a>
*/
public static final String X_GITHUB_DELIVERY = "X-GitHub-Delivery";

private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(threadPoolForRemoting);

Expand Down Expand Up @@ -117,8 +123,11 @@ public List<Item> reRegisterAllHooks() {
@SuppressWarnings("unused")
@RequirePostWithGHHookPayload
public void doIndex(@NonNull @GHEventHeader GHEvent event, @NonNull @GHEventPayload String payload) {
var currentRequest = Stapler.getCurrentRequest2();
// during unit tests currentRequest is null
String eventGuid = currentRequest != null ? currentRequest.getHeader(X_GITHUB_DELIVERY) : null;
GHSubscriberEvent subscriberEvent =
new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest2()), event, payload);
new GHSubscriberEvent(eventGuid, SCMEvent.originOf(currentRequest), event, payload);
from(GHEventsSubscriber.all())
.filter(isInterestedIn(event))
.transform(processEvent(subscriberEvent)).toList();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.jenkinsci.plugins.github.admin;

import hudson.Extension;
import hudson.model.AdministrativeMonitor;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.github.Messages;
import org.jenkinsci.plugins.github.webhook.subscriber.DuplicateEventsSubscriber;

@SuppressWarnings("unused")
@Extension
public class GitHubDuplicateEventsMonitor extends AdministrativeMonitor {

@Override
public String getDisplayName() {
return Messages.duplicate_events_administrative_monitor_displayname();
}

public String getDescription() {
return Messages.duplicate_events_administrative_monitor_description();
}

public String getBlurb() {
return Messages.duplicate_events_administrative_monitor_blurb();
}

@Override
public boolean isActivated() {
return !DuplicateEventsSubscriber.getDuplicateEventCounts().isEmpty();
}

@Override
public boolean hasRequiredPermission() {
return Jenkins.get().hasPermission(Jenkins.SYSTEM_READ);
}

@Override
public void checkRequiredPermission() {
Jenkins.get().checkPermission(Jenkins.SYSTEM_READ);
}

Check warning on line 39 in src/main/java/org/jenkinsci/plugins/github/admin/GitHubDuplicateEventsMonitor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 38-39 are not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,37 @@ public class GHSubscriberEvent extends SCMEvent<String> {
*/
private final GHEvent ghEvent;

private final String eventGuid;

/**
* Constructs a new {@link GHSubscriberEvent}.
*
* @param origin the origin (see {@link SCMEvent#originOf(HttpServletRequest)}) or {@code null}.
* @param ghEvent the type of event received from GitHub.
* @param payload the event payload.
*/
@Deprecated
public GHSubscriberEvent(@CheckForNull String origin, @NonNull GHEvent ghEvent, @NonNull String payload) {
super(Type.UPDATED, payload, origin);
this.ghEvent = ghEvent;
this.eventGuid = null;
}

/**
* Constructs a new {@link GHSubscriberEvent}.
* @param eventGuid the globally unique identifier (GUID) to identify the event.
* @param origin the origin (see {@link SCMEvent#originOf(HttpServletRequest)}) or {@code null}.
* @param ghEvent the type of event received from GitHub.
* @param payload the event payload.
*/
public GHSubscriberEvent(
@CheckForNull String eventGuid,
@CheckForNull String origin,
@NonNull GHEvent ghEvent,
@NonNull String payload) {
super(Type.UPDATED, payload, origin);
this.ghEvent = ghEvent;
this.eventGuid = eventGuid;
}

/**
Expand All @@ -39,4 +60,8 @@ public GHEvent getGHEvent() {
return ghEvent;
}

@CheckForNull
public String getEventGuid() {
return eventGuid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package org.jenkinsci.plugins.github.webhook.subscriber;

import static com.google.common.collect.Sets.immutableEnumSet;

import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import hudson.model.Item;
import hudson.model.PeriodicWork;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.kohsuke.github.GHEvent;

@Extension
public final class DuplicateEventsSubscriber extends GHEventsSubscriber {

private static final Logger LOGGER = Logger.getLogger(DuplicateEventsSubscriber.class.getName());
private static final Map<String, EventCountWithTTL> EVENT_COUNTS_TRACKER = new ConcurrentHashMap<>();
private static final long TTL_MILLIS = TimeUnit.MINUTES.toMillis(10);

@VisibleForTesting
record EventCountWithTTL(int count, long lastUpdated) { }

/**
* This method is retained only because it is an abstract method.
* It is no longer used to determine event delivery to subscribers.
* Instead, {@link #isInterestedIn} and {@link #events()} are now used to
* decide whether an event should be delivered to a subscriber.
* @see com.cloudbees.jenkins.GitHubWebHook#doIndex
*/
@Override
protected boolean isApplicable(@Nullable Item item) {
return false;
}

/**
* Subscribe to events that can trigger some kind of action within Jenkins, such as repository scan, build launch,
* etc.
* <p>
* There are about 63 specific events mentioned in the {@link GHEvent} enum, but not all of them are useful in
* Jenkins. Subscribing to and tracking them in duplicates tracker would cause an increase in memory usage, and
* those events' occurrences are likely larger than those that cause an action in Jenkins.
* <p>
* <a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads">
* Documentation reference (as also referenced in {@link GHEvent})</a>
* */
@Override
protected Set<GHEvent> events() {
return immutableEnumSet(
GHEvent.CHECK_RUN, // associated with GitHub action Re-run button to trigger build
GHEvent.CHECK_SUITE, // associated with GitHub action Re-run button to trigger build
GHEvent.CREATE, // branch or tag creation
GHEvent.DELETE, // branch or tag deletion
GHEvent.PULL_REQUEST, // PR creation (also PR close or merge)
GHEvent.PUSH // commit push
);
}

@Override
protected void onEvent(final GHSubscriberEvent event) {
String eventGuid = event.getEventGuid();
if (eventGuid == null) {
return;
}
long now = Instant.now().toEpochMilli();
EVENT_COUNTS_TRACKER.compute(
eventGuid, (key, value) -> new EventCountWithTTL(value == null ? 1 : value.count() + 1, now));
}

public static Map<String, Integer> getDuplicateEventCounts() {
return EVENT_COUNTS_TRACKER.entrySet().stream()
.filter(entry -> entry.getValue().count() > 1)
.collect(Collectors.toMap(
Map.Entry::getKey, entry -> entry.getValue().count()));
}

@VisibleForTesting
static void cleanUpOldEntries(long now) {
EVENT_COUNTS_TRACKER
.entrySet()
.removeIf(entry -> (now - entry.getValue().lastUpdated()) > TTL_MILLIS);
}

/**
* Only for testing purpose
*/
@VisibleForTesting
static Map<String, EventCountWithTTL> getEventCountsTracker() {
return new ConcurrentHashMap<>(EVENT_COUNTS_TRACKER);
}

@SuppressWarnings("unused")
@Extension
public static class EventCountTrackerCleanup extends PeriodicWork {

/**
* At present, as the {@link #TTL_MILLIS} is set to 10 minutes, we consider half of it for cleanup.
* This recurrence period is chosen to balance removing stale entries from accumulating in memory vs.
* additional load on Jenkins due to a new periodic job execution.
* <p>
* If we want to keep the stale entries to a minimum, there appear to be three different ways to achieve this:
* <ul>
* <li>Increasing the frequency of this periodic task, which will contribute to load</li>
* <li>Event-driven cleanup: for every event from GH, clean up expired entries (need to use
* better data structures and algorithms; simply calling the current {@link #cleanUpOldEntries} will
* result in {@code O(n)} for every {@code insert}, which may lead to slowness in this hot code path)</li>
* <li>Adaptive cleanup: based on the number of stale entries being seen, the system itself will adjust
* the periodic task's frequency (if such adaptive scheduling does not already exist in Jenkins core,
* this wouldn't be a good idea to implement here)
* </li>
* </ul>
*/
@Override
public long getRecurrencePeriod() {
return TTL_MILLIS / 2;
}

@Override
protected void doRun() {
LOGGER.log(
Level.FINE,
() -> "Cleaning up entries older than " + TTL_MILLIS + "ms, remaining entries: "
+ EVENT_COUNTS_TRACKER.size());

Check warning on line 131 in src/main/java/org/jenkinsci/plugins/github/webhook/subscriber/DuplicateEventsSubscriber.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 130-131 are not covered by tests
cleanUpOldEntries(Instant.now().toEpochMilli());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ github.trigger.check.method.warning.details=The webhook for repo {0}/{1} on {2}
More info can be found on the global configuration page. This message will be dismissed if Jenkins receives \
a PING event from repo webhook or if you add the repo to the ignore list in the global configuration.
unknown.error=Unknown error
duplicate.events.administrative.monitor.displayname=GitHub Duplicate Events
duplicate.events.administrative.monitor.description=Warns about duplicate events received from GitHub.
duplicate.events.administrative.monitor.blurb=Duplicate events were received from GitHub, possibly due to \
misconfiguration (e.g., multiple webhooks targeting the same Jenkins controller at the repository or organization \
level), potentially causing redundant job executions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core">
<j:out value="${it.description}"/>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<div class="alert alert-warning">
<form method="post" action="${rootURL}/${it.url}/disable" name="${it.id}">
<f:submit primary="false" clazz="jenkins-!-destructive-color" name="no" value="${%Dismiss}"/>
</form>
<j:out value="${it.blurb}"/>
</div>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.jenkinsci.plugins.github.admin;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;

import java.io.IOException;
import java.net.URL;

import org.htmlunit.HttpMethod;

import org.htmlunit.WebRequest;
import org.jenkinsci.plugins.github.Messages;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github.webhook.subscriber.DuplicateEventsSubscriber;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.WebClient;
import org.mockito.Mockito;

public class GitHubDuplicateEventsMonitorTest {

@Rule
public JenkinsRule j = new JenkinsRule();
private WebClient wc;

@Before
public void setUp() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
wc = j.createWebClient();
wc.login("admin", "admin");
}

@Test
public void testAdminMonitorDisplaysForDuplicateEvents() throws Exception {
try (var mockSubscriber = Mockito.mockStatic(GHEventsSubscriber.class)) {
var subscribers = j.jenkins.getExtensionList(GHEventsSubscriber.class);
var nonDuplicateSubscribers = subscribers.stream()
.filter(e -> !(e instanceof DuplicateEventsSubscriber))
.toList();
nonDuplicateSubscribers.forEach(subscribers::remove);
mockSubscriber.when(GHEventsSubscriber::all).thenReturn(subscribers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the test not work the same if you just delete all of this? Why do you need to remove other subscribers in this context?

Copy link
Contributor Author

@gbhat618 gbhat618 Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I am sending the push type events, it will also get delivered DefaultPushGHEventSubscriber to the push event,
In there an NPE is occurring due to it trying to process the event. (I have tried to fix the payload for the key shown in the log repository, but then it errors in other place)

log
   9.233 [id=84]	SEVERE	o.j.p.g.e.GHEventsSubscriber$4#applyNullSafe: Subscriber org.jenkinsci.plugins.github.webhook.subscriber.DefaultPushGHEventSubscriber failed to process SCMEvent{type=UPDATED, timestamp=Tue Feb 11 07:37:12 IST 2025, payload={"payload":"event1"}, origin=127.0.0.1 ⇒ http://localhost:52599/jenkins/github-webhook/} hook, skipping...
java.lang.NullPointerException: Cannot invoke "org.kohsuke.github.GHRepository.getUrl()" because the return value of "org.kohsuke.github.GHEventPayload$Push.getRepository()" is null
	at org.jenkinsci.plugins.github.webhook.subscriber.DefaultPushGHEventSubscriber.onEvent(DefaultPushGHEventSubscriber.java:76)
	at org.jenkinsci.plugins.github.extension.GHEventsSubscriber$4.applyNullSafe(GHEventsSubscriber.java:241)
	at org.jenkinsci.plugins.github.extension.GHEventsSubscriber$4.applyNullSafe(GHEventsSubscriber.java:237)
	at org.jenkinsci.plugins.github.util.misc.NullSafeFunction.apply(NullSafeFunction.java:18)
	at com.google.common.collect.Iterators$6.transform(Iterators.java:829)
	at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:52)
	at com.google.common.collect.Iterators.addAll(Iterators.java:366)
	at com.google.common.collect.Lists.newArrayList(Lists.java:154)
	at com.google.common.collect.Lists.newArrayList(Lists.java:139)
	at org.jenkinsci.plugins.github.util.FluentIterableWrapper.toList(FluentIterableWrapper.java:148)
	at com.cloudbees.jenkins.GitHubWebHook.doIndex(GitHubWebHook.java:132)
	at java.base/java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:732)
	at org.kohsuke.stapler.Function$MethodFunction.invoke(Function.java:484)
	at org.kohsuke.stapler.Function$InstanceFunction.invoke(Function.java:497)
	at org.jenkinsci.plugins.github.webhook.RequirePostWithGHHookPayload$Processor.invoke(RequirePostWithGHHookPayload.java:80)
	at org.kohsuke.stapler.PreInvokeInterceptedFunction.invoke(PreInvokeInterceptedFunction.java:26)
	at org.kohsuke.stapler.Function.bindAndInvoke(Function.java:218)
	at org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:140)
	at org.kohsuke.stapler.IndexDispatcher.dispatch(IndexDispatcher.java:31)
	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:800)
	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:938)
	at org.kohsuke.stapler.MetaClass$10.dispatch(MetaClass.java:590)
	at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:800)
	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:938)
	at org.kohsuke.stapler.Stapler.invoke(Stapler.java:721)
	at org.kohsuke.stapler.Stapler.service(Stapler.java:253)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:587)
	at org.eclipse.jetty.ee9.servlet.ServletHolder.handle(ServletHolder.java:765)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1668)
	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:204)
	at jenkins.util.HttpServletFilter$1.doFilter(HttpServletFilter.java:77)
	at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:201)
	at hudson.util.PluginServletFilter.doFilter(PluginServletFilter.java:207)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at jenkins.ErrorAttributeFilter.doFilter(ErrorAttributeFilter.java:29)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at com.cloudbees.jenkins.GitHubWebHookCrumbExclusion.process(GitHubWebHookCrumbExclusion.java:29)
	at hudson.security.csrf.CrumbFilter.doFilter(CrumbFilter.java:128)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:94)
	at jenkins.security.AcegiSecurityExceptionFilter.doFilter(AcegiSecurityExceptionFilter.java:52)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at hudson.security.UnwrapSecurityExceptionFilter.doFilter(UnwrapSecurityExceptionFilter.java:54)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:110)
	at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:101)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at jenkins.security.BasicHeaderProcessor.doFilter(BasicHeaderProcessor.java:98)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:117)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
	at hudson.security.HttpSessionContextIntegrationFilter2.doFilter(HttpSessionContextIntegrationFilter2.java:63)
	at hudson.security.ChainedServletFilter2$1.doFilter(ChainedServletFilter2.java:99)
	at hudson.security.ChainedServletFilter2.doFilter(ChainedServletFilter2.java:111)
	at hudson.security.HudsonFilter.doFilter(HudsonFilter.java:173)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at org.kohsuke.stapler.UncaughtExceptionFilter.doFilter(UncaughtExceptionFilter.java:26)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at hudson.util.CharacterEncodingFilter.doFilter(CharacterEncodingFilter.java:86)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at org.kohsuke.stapler.DiagnosticThreadNameFilter.doFilter(DiagnosticThreadNameFilter.java:31)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at jenkins.security.SuspiciousRequestFilter.doFilter(SuspiciousRequestFilter.java:38)
	at org.eclipse.jetty.ee9.servlet.FilterHolder.doFilter(FilterHolder.java:202)
	at org.eclipse.jetty.ee9.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1638)
	at org.eclipse.jetty.ee9.servlet.ServletHandler.doHandle(ServletHandler.java:526)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.handle(ScopedHandler.java:127)
	at org.eclipse.jetty.ee9.security.SecurityHandler.handle(SecurityHandler.java:574)
	at org.eclipse.jetty.ee9.nested.HandlerWrapper.handle(HandlerWrapper.java:124)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.nextHandle(ScopedHandler.java:197)
	at org.eclipse.jetty.ee9.nested.SessionHandler.doHandle(SessionHandler.java:612)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.nextHandle(ScopedHandler.java:195)
	at org.eclipse.jetty.ee9.nested.ContextHandler.doHandle(ContextHandler.java:1036)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.nextScope(ScopedHandler.java:164)
	at org.eclipse.jetty.ee9.servlet.ServletHandler.doScope(ServletHandler.java:483)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.nextScope(ScopedHandler.java:162)
	at org.eclipse.jetty.ee9.nested.SessionHandler.doScope(SessionHandler.java:589)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.nextScope(ScopedHandler.java:162)
	at org.eclipse.jetty.ee9.nested.ContextHandler.doScope(ContextHandler.java:957)
	at org.eclipse.jetty.ee9.nested.ScopedHandler.handle(ScopedHandler.java:125)
	at org.eclipse.jetty.ee9.nested.ContextHandler.handle(ContextHandler.java:1695)
	at org.eclipse.jetty.ee9.nested.HttpChannel$RequestDispatchable.dispatch(HttpChannel.java:1576)
	at org.eclipse.jetty.ee9.nested.HttpChannel.dispatch(HttpChannel.java:738)
	at org.eclipse.jetty.ee9.nested.HttpChannel.handle(HttpChannel.java:511)
	at org.eclipse.jetty.ee9.nested.ContextHandler$CoreContextHandler$CoreToNestedHandler.handle(ContextHandler.java:2863)
	at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060)
	at org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:597)
	at org.eclipse.jetty.server.Server.handle(Server.java:182)
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:662)
	at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:414)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:478)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:441)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:293)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:201)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:311)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
	at java.base/java.lang.Thread.run(Thread.java:833)

Although I could use an event type which no other existing subscriber is subscribing to, that would work.
But then what if somebody adds a subscriber to the event which I chose, so thought to remove the other subscribers..

like, changing

wc.addRequestHeader("X-Github-Event", "push");

to

wc.addRequestHeader("X-Github-Event", "check_run");

works without mock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently I added a comment to note about this reasoning.


// to begin with, monitor doesn't show automatically
assertMonitorNotDisplayed();

// normal case: unique events don't cause admin monitor
sendGHEvents(wc, "event1");
sendGHEvents(wc, "event2");
assertMonitorNotDisplayed();

// duplicate events cause admin monitor
sendGHEvents(wc, "event3");
sendGHEvents(wc, "event3");
assertMonitorDisplayed();
}
}

private void sendGHEvents(WebClient wc, String eventGuid) throws IOException {
wc.addRequestHeader("Content-Type", "application/json");
wc.addRequestHeader("X-GitHub-Delivery", eventGuid);
wc.addRequestHeader("X-Github-Event", "push");
String url = j.getURL() + "/github-webhook/";
String content = """
{
"repository":
{
"url": "http://dummy",
"html_url": "http://dummy"
},
"pusher":
{
"name": "dummy",
"email": "[email protected]"
}
}
""";
var webRequest = new WebRequest(new URL(url), HttpMethod.POST);
webRequest.setRequestBody(content);
wc.getPage(webRequest).getWebResponse();
}

private void assertMonitorNotDisplayed() throws IOException {
String manageUrl = j.getURL() + "/manage";
assertThat(
wc.getPage(manageUrl).getWebResponse().getContentAsString(),
not(containsString(Messages.duplicate_events_administrative_monitor_blurb())));
}

private void assertMonitorDisplayed() throws IOException {
String manageUrl = j.getURL() + "/manage";
assertThat(
wc.getPage(manageUrl).getWebResponse().getContentAsString(),
containsString(Messages.duplicate_events_administrative_monitor_blurb()));
}
}
Loading
Loading