8000 zammad webhooks, part1: store them in the database (#23) · EuroPython/internal-bot@80eea54 · GitHub
[go: up one dir, main page]

Skip to content

Commit 80eea54

Browse files
authored
zammad webhooks, part1: store them in the database (#23)
* zammad webhooks, part1: store them in the database * review feedback
1 parent 64bcafd commit 80eea54

File tree

6 files changed

+229
-13
lines changed

6 files changed

+229
-13
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ worker:
5858
test:
5959
$(TEST_CMD) -s -v
6060

61+
test_last_failed:
62+
$(TEST_CMD) -s -v --last-failed
63+
64+
test_: test_last_failed
65+
6166
test/k:
6267
$(TEST_CMD) -s -v -k $(K)
6368

intbot/core/endpoints/webhooks.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def internal_webhook_endpoint(request):
2323
content=json.loads(request.body),
2424
extra={},
2525
)
26+
# Schedule a task for the worker to process the webhook outside of
27+
# request/response cycle.
2628
process_webhook.enqueue(str(wh.uuid))
2729

2830
return JsonResponse({"status": "created", "guid": wh.uuid})
@@ -45,24 +47,26 @@ def verify_internal_webhook(request):
4547
@csrf_exempt
4648
def github_webhook_endpoint(request):
4749
if request.method == "POST":
48-
github_headers = {
49-
k: v for k, v in request.headers.items() if k.startswith("X-Github")
50-
}
51-
5250
try:
5351
signature = verify_github_signature(request)
5452
except ValueError as e:
5553
return HttpResponseForbidden(e)
5654

55+
github_headers = {
56+
k: v for k, v in request.headers.items() if k.startswith("X-Github")
57+
}
58+
5759
wh = Webhook.objects.create(
5860
source="github",
5961
meta=github_headers,
6062
signature=signature,
6163
content=json.loads(request.body),
6264
extra={},
6365
)
66+
# Schedule a task for the worker to process the webhook outside of
67+
# request/response cycle.
6468
process_webhook.enqueue(str(wh.uuid))
65-
return JsonResponse({"status": "ok"})
69+
return JsonResponse({"status": "created", "guid": wh.uuid})
6670

6771
return HttpResponseNotAllowed("Only POST")
6872

@@ -84,6 +88,55 @@ def verify_github_signature(request) -> str:
8488
expected = "sha256=" + hashed.hexdigest()
8589

8690
if not hmac.compare_digest(expected, signature):
87-
raise ValueError("Signature's don't match")
91+
raise ValueError("Signatures don't match")
92+
93+
return signature
94+
95+
96+
@csrf_exempt
97+
def zammad_webhook_endpoint(request):
98+
if request.method == "POST":
99+
try:
100+
signature = verify_zammad_signature(request)
101+
except ValueError as e:
102+
return HttpResponseForbidden(e)
103+
104+
zammad_headers = {
105+
k: v for k, v in request.headers.items() if k.startswith("X-Zammad")
106+
}
107+
108+
wh = Webhook.objects.create(
109+
source="zammad",
110+
meta=zammad_headers,
111+
signature=signature,
112+
content=json.loads(request.body),
113+
extra={},
114+
)
115+
# Schedule a task for the worker to process the webhook outside of
116+
# request/response cycle.
117+
process_webhook.enqueue(str(wh.uuid))
118+
return JsonResponse({"status": "created", "guid": wh.uuid})
119+
120+
return HttpResponseNotAllowed("Only POST")
121+
122+
123+
def verify_zammad_signature(request) -> str:
124+
"""Verify that the payload was sent by our zammad"""
125+
126+
if "X-Hub-Signature" not in request.headers:
127+
raise ValueError("X-Hub-Signature is missing")
128+
129+
signature = request.headers["X-Hub-Signature"]
130+
131+
hashed = hmac.new(
132+
settings.ZAMMAD_WEBHOOK_SECRET_TOKEN.encode("utf-8"),
133+
msg=request.body,
134+
digestmod=hashlib.sha1,
135+
)
136+
137+
expected = "sha1=" + hashed.hexdigest()
138+
139+
if not hmac.compare_digest(expected, signature):
140+
raise ValueError("Signatures don't match")
88141

89142
return signature

intbot/core/tasks.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ def process_webhook(wh_uuid: str):
1919
elif wh.source == "github":
2020
process_github_webhook(wh)
2121

22+
elif wh.source == "zammad":
23+
process_zammad_webhook(wh)
24+
2225
else:
2326
raise ValueError(f"Unsupported source {wh.source}")
2427

@@ -32,8 +35,6 @@ def process_internal_webhook(wh: Webhook):
3235
DiscordMessage.objects.create(
3336
channel_id=channel.channel_id,
3437
channel_name=channel.channel_name,
35-
# channel_id=settings.DISCORD_TEST_CHANNEL_ID,
36-
# channel_name=settings.DISCORD_TEST_CHANNEL_NAME,
3738
content=f"Webhook content: {wh.content}",
3839
# Mark as not sent - to be sent with the next batch
3940
sent_at=None,
@@ -70,3 +71,9 @@ def process_github_webhook(wh: Webhook):
7071
)
7172
wh.processed_at = timezone.now()
7273
wh.save()
74+
75+
76+
def process_zammad_webhook(wh: Webhook):
77+
# NOTE(artcz) Do nothing for now. Just a placeholder.
78+
# Processing will come in the next PR.
79+
return

intbot/intbot/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ def get(name) -> str:
167167
GITHUB_EP2025_PROJECT_ID = get("GITHUB_EP2025_PROJECT_ID")
168168
GITHUB_EM_PROJECT_ID = get("GITHUB_EM_PROJECT_ID")
169169

170+
# Zammad
171+
ZAMMAD_WEBHOOK_SECRET_TOKEN = get("ZAMMAD_WEBHOOK_SECRET_TOKEN")
172+
170173
if DJANGO_ENV == "dev":
171174
DEBUG = True
172175
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]

intbot/intbot/urls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from core.endpoints.basic import index
2-
from core.endpoints.webhooks import github_webhook_endpoint, internal_webhook_endpoint
2+
from core.endpoints.webhooks import (
3+
github_webhook_endpoint,
4+
internal_webhook_endpoint,
5+
zammad_webhook_endpoint,
6+
)
37
from django.contrib import admin
48
from django.urls import path
59

@@ -9,4 +13,5 @@
913
# Internal Webhooks
1014
path("webhook/internal/", internal_webhook_endpoint),
1115
path("webhook/github/", github_webhook_endpoint),
16+
path("webhook/zammad/", zammad_webhook_endpoint),
1217
]

intbot/tests/test_webhooks.py

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import hashlib
2+
import hmac
3+
import json
4+
15
import pytest
2-
from django.conf import settings
36
from core.models import Webhook
7+
from django.conf import settings
48

59

610
@pytest.mark.django_db
@@ -14,7 +18,7 @@ def test_internal_wh_endpoint_checks_authorization_token(client):
1418

1519
response = client.post(
1620
"/webhook/internal/",
17-
json=webhook_body,
21+
json.dumps(webhook_body),
1822
content_type="application/json",
1923
)
2024

@@ -35,7 +39,7 @@ def test_internal_wh_endpoint_fails_with_bad_token(client):
3539

3640
response = client.post(
3741
"/webhook/internal/",
38-
json=webhook_body,
42+
json.dumps(webhook_body),
3943
content_type="application/json",
4044
HTTP_AUTHORIZATION="random-incorrect-token",
4145
)
@@ -57,7 +61,7 @@ def test_internal_wh_endpoint_works_with_correct_token(client):
5761

5862
response = client.post(
5963
"/webhook/internal/",
60-
json=webhook_body,
64+
json.dumps(webhook_body),
6165
content_type="application/json",
6266
HTTP_AUTHORIZATION=settings.WEBHOOK_INTERNAL_TOKEN,
6367
)
@@ -67,3 +71,142 @@ def test_internal_wh_endpoint_works_with_correct_token(client):
6771
assert response["Content-Type"] == "application/json"
6872
assert response.json()["status"] == "created"
6973
assert response.json()["guid"] == str(wh.uuid)
74+
75+
76+
@pytest.mark.django_db
77+
def test_github_webhook_endpoint_checks_authorization_token(client):
78+
webhook_body = {}
79+
response = client.post(
80+
"/webhook/github/",
81+
json.dumps(webhook_body),
82+
content_type="application/json",
83+
)
84+
85+
assert response.status_code == 403
86+
assert response.content == "X-Hub-Signature-256 is missing".encode("utf-8")
87+
88+
def sign_github_webhook(webhook_body):
89+
hashed = hmac.new(
90+
settings.GITHUB_WEBHOOK_SECRET_TOKEN.encode("utf-8"),
91+
msg=json.dumps(webhook_body).encode("utf-8"),
92+
digestmod=hashlib.sha256,
93+
)
94+
signature = "sha256=" + hashed.hexdigest()
95+
96+
return signature
97+
98+
99+
@pytest.mark.django_db
100+
def test_github_webhook_endpoint_fails_with_bad_token(client):
101+
webhook_body = {
102+
"event": "test1",
103+
"content": {
104+
"random": "content",
105+
},
106+
}
107+
108+
response = client.post(
109+
"/webhook/github/",
110+
json.dumps(webhook_body),
111+
content_type="application/json",
112+
headers={"X-Hub-Signature-256": "bad signature"},
113+
)
114+
115+
assert response.status_code == 403
116+
assert response.content == "Signatures don't match".encode("utf-8")
117+
assert True
118+
119+
120+
@pytest.mark.django_db
121+
def test_github_webhook_endpoint_works_with_correct_token(client):
122+
webhook_body = {
123+
"event": "test1",
124+
"content": {
125+
"random": "content",
126+
},
127+
}
128+
129+
signature = sign_github_webhook(webhook_body)
130+
131+
response = client.post(
132+
"/webhook/github/",
133+
json.dumps(webhook_body),
134+
content_type="application/json",
135+
headers={"X-Hub-Signature-256": signature},
136+
)
137+
assert response.status_code == 200
138+
wh = Webhook.objects.get()
139+
assert response["Content-Type"] == "application/json"
140+
assert response.json()["status"] == "created"
141+
assert response.json()["guid"] == str(wh.uuid)
142+
assert wh.source == "github"
143+
144+
145+
def sign_zammad_webhook(webhook_body):
146+
hashed = hmac.new(
147+
settings.ZAMMAD_WEBHOOK_SECRET_TOKEN.encode("utf-8"),
148+
msg=json.dumps(webhook_body).encode("utf-8"),
149+
digestmod=hashlib.sha1,
150+
)
151+
signature = "sha1=" + hashed.hexdigest()
152+
153+
return signature
154+
155+
156+
@pytest.mark.django_db
157+
def test_zammad_webhook_endpoint_checks_authorization_token(client):
158+
webhook_body = {}
159+
160+
response = client.post(
161+
"/webhook/zammad/",
162+
json.dumps(webhook_body),
163+
content_type="application/json",
164+
)
165+
166+
assert response.status_code == 403
167+
assert response.content == "X-Hub-Signature is missing".encode("utf-8")
168+
169+
170+
@pytest.mark.django_db
171+
def test_zammad_webhook_endpoint_fails_with_bad_token(client):
172+
webhook_body = {
173+
"event": "test1",
174+
"content": {
175+
"random": "content",
176+
},
177+
}
178+
179+
response = client.post(
180+
"/webhook/zammad/",
181+
json.dumps(webhook_body),
182+
content_type="application/json",
183+
headers={"X-Hub-Signature": "bad signature"},
184+
)
185+
186+
assert response.status_code == 403
187+
assert response.content == "Signatures don't match".encode("utf-8")
188+
189+
190+
@pytest.mark.django_db
191+
def test_zammad_webhook_endpoint_works_with_correct_token(client):
192+
webhook_body = {
193+
"event": "test1",
194+
"content": {
195+
"random": "content",
196+
},
197+
}
198+
199+
signature = sign_zammad_webhook(webhook_body)
200+
201+
response = client.post(
202+
"/webhook/zammad/",
203+
json.dumps(webhook_body),
204+
content_type="application/json",
205+
headers={"X-Hub-Signature": signature},
206+
)
207+
assert response.status_code == 200
208+
wh = Webhook.objects.get()
209+
assert response["Content-Type"] == "application/json"
210+
assert response.json()["status"] == "created"
211+
assert response.json()["guid"] == str(wh.uuid)
212+
assert wh.source == "zammad"

0 commit comments

Comments
 (0)
0