Skip to content

Commit eeafd54

Browse files
committed
feat: Customizable HTTP headers for webhooks (OD-2702)
1 parent 2a22ef4 commit eeafd54

File tree

9 files changed

+408
-60
lines changed

9 files changed

+408
-60
lines changed

server-core/src/main/java/io/onedev/server/data/migration/DataMigrator.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8524,8 +8524,12 @@ private void migrate221(File dataDir, Stack<Integer> versions) {
85248524
for (File file : dataDir.listFiles()) {
85258525
if (file.getName().startsWith("Projects.xml")) {
85268526
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
8527-
for (Element element : dom.getRootElement().elements()) {
8528-
element.addElement("aiSetting");
8527+
for (Element projectElement : dom.getRootElement().elements()) {
8528+
projectElement.addElement("aiSetting");
8529+
Element webHooksElement = projectElement.element("webHooks");
8530+
for (Element webHookElement : webHooksElement.elements()) {
8531+
webHookElement.addElement("headers");
8532+
}
85298533
}
85308534
dom.writeToFile(file, false);
85318535
}

server-core/src/main/java/io/onedev/server/model/support/WebHook.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public boolean includes(Object event) {
7979

8080
private String secret = CryptoUtils.generateSecret();
8181

82+
private List<WebHookHeader> headers = new ArrayList<>();
83+
8284
@Editable(order=100, description="The URL of the server endpoint that will receive the webhook POST requests")
8385
@NotEmpty
8486
public String getPostUrl() {
@@ -109,5 +111,15 @@ public String getSecret() {
109111
public void setSecret(String secret) {
110112
this.secret = secret;
111113
}
114+
115+
@Editable(order=400, name="Custom Headers", description="Optionally specify additional HTTP headers to include in "
116+
+ "the webhook POST request, for example an Authorization header required by the receiving endpoint")
117+
public List<WebHookHeader> getHeaders() {
118+
return headers;
119+
}
120+
121+
public void setHeaders(List<WebHookHeader> headers) {
122+
this.headers = headers;
123+
}
112124

113125
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.onedev.server.model.support;
2+
3+
import java.io.Serializable;
4+
5+
import javax.validation.constraints.NotEmpty;
6+
7+
import io.onedev.server.annotation.Editable;
8+
9+
@Editable
10+
public class WebHookHeader implements Serializable {
11+
12+
private static final long serialVersionUID = 1L;
13+
14+
private String name;
15+
16+
private String value;
17+
18+
@Editable(order=100, name="Header Name")
19+
@NotEmpty
20+
public String getName() {
21+
return name;
22+
}
23+
24+
public void setName(String name) {
25+
this.name = name;
26+
}
27+
28+
@Editable(order=200, name="Header Value")
29+
@NotEmpty
30+
public String getValue() {
31+
return value;
32+
}
33+
34+
public void setValue(String value) {
35+
this.value = value;
36+
}
37+
38+
}

server-core/src/main/java/io/onedev/server/notification/WebHookManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ public void on(ProjectEvent event) {
6363
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON);
6464
httpPost.setHeader(HttpHeaders.ACCEPT_CHARSET, UTF_8.name());
6565
httpPost.setHeader(SIGNATURE_HEAD, webHook.getSecret());
66+
for (var header : webHook.getHeaders())
67+
httpPost.setHeader(header.getName(), header.getValue());
6668

6769
try (var response = client.execute(httpPost)) {
6870
HttpEntity responseEntity = response.getEntity();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<wicket:panel>
2+
<form wicket:id="form" class="webhook-edit leave-confirm">
3+
<div class="modal-header">
4+
<h5 id="modal-title" class="modal-title"><wicket:t>Web Hook</wicket:t></h5>
5+
<button wicket:id="close" type="button" class="close"><wicket:svg href="times" class="icon"/></button>
6+
</div>
7+
<div class="modal-body">
8+
<div wicket:id="editor"></div>
9+
</div>
10+
<div class="modal-footer">
11+
<input wicket:id="save" type="submit" class="dirty-aware btn btn-primary" t:value="Save">
12+
<a wicket:id="cancel" class="btn btn-secondary"><wicket:t>Cancel</wicket:t></a>
13+
</div>
14+
</form>
15+
</wicket:panel>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package io.onedev.server.web.page.project.setting.webhook;
2+
3+
import org.apache.commons.lang3.SerializationUtils;
4+
import org.apache.wicket.ajax.AjaxRequestTarget;
5+
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
6+
import org.apache.wicket.ajax.markup.html.AjaxLink;
7+
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
8+
import org.apache.wicket.markup.html.form.Form;
9+
import org.apache.wicket.markup.html.panel.Panel;
10+
import org.apache.wicket.request.cycle.RequestCycle;
11+
12+
import io.onedev.server.OneDev;
13+
import io.onedev.server.data.migration.VersionedXmlDoc;
14+
import io.onedev.server.model.Project;
15+
import io.onedev.server.model.support.WebHook;
16+
import io.onedev.server.service.AuditService;
17+
import io.onedev.server.service.ProjectService;
18+
import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener;
19+
import io.onedev.server.web.editable.BeanContext;
20+
21+
abstract class WebHookEditPanel extends Panel {
22+
23+
private final int hookIndex;
24+
25+
public WebHookEditPanel(String id, int hookIndex) {
26+
super(id);
27+
this.hookIndex = hookIndex;
28+
}
29+
30+
@Override
31+
protected void onInitialize() {
32+
super.onInitialize();
33+
34+
WebHook hook = hookIndex != -1
35+
? SerializationUtils.clone(getProject().getWebHooks().get(hookIndex))
36+
: new WebHook();
37+
38+
Form<?> form = new Form<Void>("form") {
39+
40+
@Override
41+
protected void onError() {
42+
super.onError();
43+
RequestCycle.get().find(AjaxRequestTarget.class).add(this);
44+
}
45+
46+
};
47+
48+
form.add(new AjaxLink<Void>("close") {
49+
50+
@Override
51+
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
52+
super.updateAjaxAttributes(attributes);
53+
attributes.getAjaxCallListeners().add(new ConfirmLeaveListener(WebHookEditPanel.this));
54+
}
55+
56+
@Override
57+
public void onClick(AjaxRequestTarget target) {
58+
onCancel(target);
59+
}
60+
61+
});
62+
63+
var editor = BeanContext.edit("editor", hook);
64+
form.add(editor);
65+
66+
form.add(new AjaxButton("save") {
67+
68+
@Override
69+
protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
70+
super.onSubmit(target, form);
71+
if (editor.isValid()) {
72+
String oldAuditContent = null;
73+
String verb;
74+
if (hookIndex != -1) {
75+
oldAuditContent = VersionedXmlDoc.fromBean(getProject().getWebHooks().get(hookIndex)).toXML();
76+
getProject().getWebHooks().set(hookIndex, hook);
77+
verb = "changed";
78+
} else {
79+
getProject().getWebHooks().add(hook);
80+
verb = "added";
81+
}
82+
var newAuditContent = VersionedXmlDoc.fromBean(hook).toXML();
83+
OneDev.getInstance(ProjectService.class).update(getProject());
84+
OneDev.getInstance(AuditService.class).audit(getProject(), verb + " web hook \"" + hook.getPostUrl() + "\"", oldAuditContent, newAuditContent);
85+
onSave(target);
86+
} else {
87+
target.add(form);
88+
}
89+
}
90+
91+
});
92+
93+
form.add(new AjaxLink<Void>("cancel") {
94+
95+
@Override
96+
protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
97+
super.updateAjaxAttributes(attributes);
98+
attributes.getAjaxCallListeners().add(new ConfirmLeaveListener(WebHookEditPanel.this));
99+
}
100+
101+
@Override
102+
public void onClick(AjaxRequestTarget target) {
103+
onCancel(target);
104+
}
105+
106+
});
107+
108+
form.setOutputMarkupId(true);
109+
add(form);
110+
}
111+
112+
protected abstract Project getProject();
113+
114+
protected abstract void onSave(AjaxRequestTarget target);
115+
116+
protected abstract void onCancel(AjaxRequestTarget target);
117+
118+
}

server-core/src/main/java/io/onedev/server/web/page/project/setting/webhook/WebHooksBean.java

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
<wicket:extend>
2-
<div class="alert alert-notice bg-white shadow mb-5 text-gray">
3-
<wicket:svg href="bulb" class="icon mr-2"/>
4-
<wicket:t>For web hooks defined here and in parent projects, OneDev will post event data in JSON format to
5-
specified URLs when subscribed events happen</wicket:t>
6-
</div>
72
<div class="card web-hooks">
83
<div class="card-body">
9-
<form wicket:id="form" class="leave-confirm">
10-
<div wicket:id="feedback" class="mb-4"></div>
11-
<div wicket:id="editor"></div>
12-
<input type="submit" t:value="Save" class="dirty-aware btn btn-primary mt-4">
13-
</form>
4+
<div class="text-muted mb-4">
5+
<wicket:svg href="bulb" class="icon"/> <wicket:t>For web hooks defined here and in parent projects, OneDev will post event data in JSON format to
6+
specified URLs when subscribed events happen</wicket:t>
7+
</div>
8+
<a wicket:id="addNew" class="btn btn-icon btn-primary mb-4" t:data-tippy-content="Add web hook"><wicket:svg href="plus" class="icon"></wicket:svg></a>
9+
<table wicket:id="webHooks" class="table table-hover"></table>
1410
</div>
1511
</div>
16-
</wicket:extend>
12+
<wicket:fragment wicket:id="urlColumnFrag">
13+
<a wicket:id="link"><span wicket:id="label"></span></a>
14+
</wicket:fragment>
15+
<wicket:fragment wicket:id="actionColumnFrag">
16+
<a wicket:id="edit" class="btn btn-xs btn-icon btn-light btn-hover-primary mr-1" t:data-tippy-content="Edit"><wicket:svg href="edit" class="icon"></wicket:svg></a>
17+
<a wicket:id="delete" class="btn btn-xs btn-icon btn-light btn-hover-danger" t:data-tippy-content="Delete"><wicket:svg href="trash" class="icon"></wicket:svg></a>
18+
</wicket:fragment>
19+
</wicket:extend>

0 commit comments

Comments
 (0)