Skip to content

Commit 4a0cb42

Browse files
fix(eco): Fixes prevent webhook forwarding body format (#103119)
1 parent 1c615f8 commit 4a0cb42

File tree

5 files changed

+155
-32
lines changed

5 files changed

+155
-32
lines changed

src/sentry/integrations/github/webhook_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from enum import StrEnum
22

33
GITHUB_WEBHOOK_TYPE_HEADER = "HTTP_X_GITHUB_EVENT"
4+
GITHUB_WEBHOOK_TYPE_HEADER_KEY = "X-GITHUB-EVENT"
5+
GITHUB_INSTALLATION_TARGET_ID_HEADER = "X-GITHUB-HOOK-INSTALLATION-TARGET-ID"
46

57

68
class GithubWebhookType(StrEnum):

src/sentry/middleware/integrations/parsers/github.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def get_response(self) -> HttpResponseBase:
106106

107107
# The overwatch forwarder implements its own region-based checks
108108
OverwatchGithubWebhookForwarder(integration=integration).forward_if_applicable(
109-
event=event, headers=self.request.META
109+
event=event, headers=self.request.headers
110110
)
111111

112112
return response

src/sentry/overwatch_webhooks/webhook_forwarder.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
from sentry import options
88
from sentry.constants import ObjectStatus
9-
from sentry.integrations.github.webhook_types import GITHUB_WEBHOOK_TYPE_HEADER, GithubWebhookType
9+
from sentry.integrations.github.webhook_types import (
10+
GITHUB_INSTALLATION_TARGET_ID_HEADER,
11+
GITHUB_WEBHOOK_TYPE_HEADER_KEY,
12+
GithubWebhookType,
13+
)
1014
from sentry.integrations.models.integration import Integration
1115
from sentry.integrations.models.organization_integration import OrganizationIntegration
1216
from sentry.models.organizationmapping import OrganizationMapping
@@ -26,10 +30,6 @@
2630
}
2731

2832

29-
GITHUB_INSTALLATION_TARGET_ID_HEADER = "X-GitHub-Hook-Installation-Target-ID"
30-
DJANGO_HTTP_GITHUB_INSTALLATION_TARGET_ID_HEADER = "HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID"
31-
32-
3333
@dataclass(frozen=True)
3434
class OverwatchOrganizationContext:
3535
organization_integration: OrganizationIntegration
@@ -51,7 +51,7 @@ def __init__(self, integration: Integration):
5151
self.integration = integration
5252

5353
def should_forward_to_overwatch(self, headers: Mapping[str, str]) -> bool:
54-
event_type = headers.get(GITHUB_WEBHOOK_TYPE_HEADER)
54+
event_type = headers.get(GITHUB_WEBHOOK_TYPE_HEADER_KEY)
5555
verbose_log(
5656
"overwatch.debug.should_forward_to_overwatch.checked",
5757
extra={
@@ -163,7 +163,7 @@ def forward_if_applicable(self, event: Mapping[str, Any], headers: Mapping[str,
163163

164164
raw_app_id = headers.get(
165165
GITHUB_INSTALLATION_TARGET_ID_HEADER,
166-
) or headers.get(DJANGO_HTTP_GITHUB_INSTALLATION_TARGET_ID_HEADER)
166+
)
167167
verbose_log(
168168
"overwatch.debug.raw_app_id",
169169
extra={"region_name": region_name, "raw_app_id": raw_app_id},
@@ -179,10 +179,12 @@ def forward_if_applicable(self, event: Mapping[str, Any], headers: Mapping[str,
179179
)
180180
app_id = None
181181

182+
formatted_headers = {k: v for k, v in headers.items()}
183+
182184
webhook_detail = WebhookDetails(
183185
organizations=org_summaries,
184186
webhook_body=dict(event),
185-
webhook_headers=headers,
187+
webhook_headers=formatted_headers,
186188
integration_provider=self.integration.provider,
187189
region=region_name,
188190
app_id=app_id,

tests/sentry/middleware/integrations/parsers/test_github.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import orjson
12
import pytest
23
import responses
34
from django.db import router, transaction
@@ -17,7 +18,7 @@
1718
from sentry.testutils.helpers.options import override_options
1819
from sentry.testutils.outbox import assert_no_webhook_payloads, assert_webhook_payloads_for_mailbox
1920
from sentry.testutils.region import override_regions
20-
from sentry.testutils.silo import control_silo_test
21+
from sentry.testutils.silo import control_silo_test, create_test_regions
2122
from sentry.types.region import Region, RegionCategory
2223

2324
region = Region("us", 1, "https://us.testserver", RegionCategory.MULTI_TENANT)
@@ -304,3 +305,115 @@ class GithubRequestParserTypeRoutingTest(GithubRequestParserTest):
304305
def setup(self):
305306
with override_options({"github.webhook-type-routing.enabled": True}):
306307
yield
308+
309+
310+
@control_silo_test(regions=create_test_regions("us"))
311+
class GithubRequestParserOverwatchForwarderTest(TestCase):
312+
factory = RequestFactory()
313+
path = reverse("sentry-integration-github-webhook")
314+
315+
@pytest.fixture(autouse=True)
316+
def setup(self):
317+
with override_options({"github.webhook-type-routing.enabled": True}):
318+
yield
319+
320+
def setUp(self) -> None:
321+
super().setUp()
322+
self.organization = self.create_organization(
323+
name="Test Org",
324+
slug="test-org",
325+
region="us",
326+
owner=self.create_user(email="test@example.com"),
327+
)
328+
self.integration = self.create_integration(
329+
provider="github",
330+
external_id="1",
331+
name="Test Integration",
332+
organization=self.organization,
333+
)
334+
335+
@responses.activate
336+
def test_overwatch_forwarder(self) -> None:
337+
with (
338+
override_options({"overwatch.enabled-regions": ["us"]}),
339+
override_settings(
340+
OVERWATCH_REGION_URLS={"us": "https://us.example.com/api"},
341+
OVERWATCH_WEBHOOK_SECRET="test-secret",
342+
),
343+
):
344+
responses.add(
345+
responses.POST,
346+
"https://us.example.com/api/webhooks/sentry",
347+
status=200,
348+
)
349+
350+
request = self.factory.post(
351+
self.path,
352+
data={"installation": {"id": "1"}, "action": "created"},
353+
content_type="application/json",
354+
headers={
355+
"x-github-event": GithubWebhookType.PULL_REQUEST.value,
356+
"x-github-hook-installation-target-id": "123",
357+
},
358+
)
359+
parser = GithubRequestParser(
360+
request=request,
361+
response_handler=lambda _: HttpResponse(status=200, content="passthrough"),
362+
)
363+
364+
response = parser.get_response()
365+
assert isinstance(response, HttpResponse)
366+
assert response.status_code == status.HTTP_202_ACCEPTED
367+
assert response.content == b""
368+
369+
assert len(responses.calls) == 1
370+
assert responses.calls[0].request.url == "https://us.example.com/api/webhooks/sentry"
371+
assert responses.calls[0].request.method == "POST"
372+
json_body = orjson.loads(responses.calls[0].request.body)
373+
374+
assert json_body["organizations"] == [
375+
{
376+
"name": "Test Org",
377+
"slug": "test-org",
378+
"id": self.organization.id,
379+
"region": "us",
380+
"github_integration_id": self.integration.id,
381+
"organization_integration_id": self.organization_integration.id,
382+
}
383+
]
384+
assert json_body["webhook_body"] == {"installation": {"id": "1"}, "action": "created"}
385+
assert json_body["app_id"] == 123
386+
assert json_body["webhook_headers"]["X-Github-Event"] == "pull_request"
387+
assert json_body["integration_provider"] == "github"
388+
assert json_body["region"] == "us"
389+
assert json_body["event_type"] == "github"
390+
391+
@responses.activate
392+
def test_overwatch_forwarder_missing_region_config(self) -> None:
393+
with (
394+
override_options({"overwatch.enabled-regions": ["us"]}),
395+
override_settings(
396+
OVERWATCH_REGION_URLS={"de": "https://de.example.com/api"},
397+
OVERWATCH_WEBHOOK_SECRET="test-secret",
398+
),
399+
):
400+
request = self.factory.post(
401+
self.path,
402+
data={"installation": {"id": "1"}, "action": "created"},
403+
content_type="application/json",
404+
headers={
405+
"x-github-event": GithubWebhookType.PULL_REQUEST.value,
406+
"x-github-hook-installation-target-id": "1",
407+
},
408+
)
409+
parser = GithubRequestParser(
410+
request=request,
411+
response_handler=lambda _: HttpResponse(status=200, content="passthrough"),
412+
)
413+
414+
response = parser.get_response()
415+
assert isinstance(response, HttpResponse)
416+
assert response.status_code == status.HTTP_202_ACCEPTED
417+
assert response.content == b""
418+
419+
assert len(responses.calls) == 0

tests/sentry/overwatch_webhooks/test_webhook_forwarder.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from django.test.utils import override_settings
77

88
from sentry.hybridcloud.models.outbox import outbox_context
9+
from sentry.integrations.github.webhook_types import (
10+
GITHUB_INSTALLATION_TARGET_ID_HEADER,
11+
GITHUB_WEBHOOK_TYPE_HEADER_KEY,
12+
)
913
from sentry.integrations.models.organization_integration import OrganizationIntegration
1014
from sentry.overwatch_webhooks.types import (
1115
DEFAULT_REQUEST_TYPE,
@@ -46,15 +50,15 @@ def test_init_creates_publisher_with_correct_provider(self):
4650

4751
def test_should_forward_to_overwatch_with_valid_events(self):
4852
for event_action in GITHUB_EVENTS_TO_FORWARD_OVERWATCH:
49-
headers = {"HTTP_X_GITHUB_EVENT": event_action}
53+
headers = {GITHUB_WEBHOOK_TYPE_HEADER_KEY: event_action}
5054
assert self.forwarder.should_forward_to_overwatch(headers) is True
5155

5256
def test_should_forward_to_overwatch_with_invalid_events(self):
5357
invalid_events = [
54-
{"HTTP_X_GITHUB_EVENT": "invalid_action"},
55-
{"HTTP_X_GITHUB_EVENT": "create"},
56-
{"HTTP_X_GITHUB_EVENT": "delete"},
57-
{"HTTP_X_GITHUB_EVENT": "some_other_action"},
58+
{GITHUB_WEBHOOK_TYPE_HEADER_KEY: "invalid_action"},
59+
{GITHUB_WEBHOOK_TYPE_HEADER_KEY: "create"},
60+
{GITHUB_WEBHOOK_TYPE_HEADER_KEY: "delete"},
61+
{GITHUB_WEBHOOK_TYPE_HEADER_KEY: "some_other_action"},
5862
{},
5963
]
6064

@@ -128,7 +132,9 @@ def test_forward_if_applicable_no_organizations(self):
128132
event = {"action": "push", "data": "test"}
129133

130134
with patch.object(OverwatchWebhookPublisher, "enqueue_webhook") as mock_enqueue:
131-
self.forwarder.forward_if_applicable(event, headers={"HTTP_X_GITHUB_EVENT": "push"})
135+
self.forwarder.forward_if_applicable(
136+
event, headers={GITHUB_WEBHOOK_TYPE_HEADER_KEY: "push"}
137+
)
132138
mock_enqueue.assert_not_called()
133139

134140
@override_options({"overwatch.enabled-regions": ["us"]})
@@ -143,7 +149,7 @@ def test_forward_if_applicable_event_not_eligible_for_forwarding(self):
143149

144150
with patch.object(OverwatchWebhookPublisher, "enqueue_webhook") as mock_enqueue:
145151
self.forwarder.forward_if_applicable(
146-
event, headers={"HTTP_X_GITHUB_EVENT": "invalid_action"}
152+
event, headers={GITHUB_WEBHOOK_TYPE_HEADER_KEY: "invalid_action"}
147153
)
148154
mock_enqueue.assert_not_called()
149155

@@ -161,8 +167,8 @@ def test_forward_if_applicable_successful_forwarding(self):
161167
self.forwarder.forward_if_applicable(
162168
event,
163169
headers={
164-
"HTTP_X_GITHUB_EVENT": "pull_request",
165-
"HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID": "987654",
170+
GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request",
171+
GITHUB_INSTALLATION_TARGET_ID_HEADER: "987654",
166172
},
167173
)
168174

@@ -188,7 +194,7 @@ def test_forward_if_applicable_multiple_organizations(self):
188194

189195
with patch.object(OverwatchWebhookPublisher, "enqueue_webhook") as mock_enqueue:
190196
self.forwarder.forward_if_applicable(
191-
event, headers={"HTTP_X_GITHUB_EVENT": "pull_request"}
197+
event, headers={GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request"}
192198
)
193199

194200
mock_enqueue.assert_called_once()
@@ -213,7 +219,7 @@ def test_forward_if_applicable_all_valid_event_actions(self):
213219

214220
with patch.object(OverwatchWebhookPublisher, "enqueue_webhook") as mock_enqueue:
215221
self.forwarder.forward_if_applicable(
216-
event, headers={"HTTP_X_GITHUB_EVENT": event_type}
222+
event, headers={GITHUB_WEBHOOK_TYPE_HEADER_KEY: event_type}
217223
)
218224
mock_enqueue.assert_called_once()
219225

@@ -246,7 +252,7 @@ def test_forward_if_applicable_preserves_webhook_body_data(self):
246252

247253
with patch.object(OverwatchWebhookPublisher, "enqueue_webhook") as mock_enqueue:
248254
self.forwarder.forward_if_applicable(
249-
complex_event, headers={"HTTP_X_GITHUB_EVENT": "pull_request"}
255+
complex_event, headers={GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request"}
250256
)
251257

252258
mock_enqueue.assert_called_once()
@@ -294,8 +300,8 @@ def test_forwards_to_correct_regions(self):
294300
self.forwarder.forward_if_applicable(
295301
event,
296302
headers={
297-
"HTTP_X_GITHUB_EVENT": "pull_request",
298-
"HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID": "987654",
303+
GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request",
304+
GITHUB_INSTALLATION_TARGET_ID_HEADER: "987654",
299305
},
300306
)
301307

@@ -319,8 +325,8 @@ def test_forwards_to_correct_regions(self):
319325
],
320326
"webhook_body": event,
321327
"webhook_headers": {
322-
"HTTP_X_GITHUB_EVENT": "pull_request",
323-
"HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID": "987654",
328+
GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request",
329+
GITHUB_INSTALLATION_TARGET_ID_HEADER: "987654",
324330
},
325331
"integration_provider": "github",
326332
"region": "us",
@@ -342,8 +348,8 @@ def test_forwards_to_correct_regions(self):
342348
],
343349
"webhook_body": event,
344350
"webhook_headers": {
345-
"HTTP_X_GITHUB_EVENT": "pull_request",
346-
"HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID": "987654",
351+
GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request",
352+
GITHUB_INSTALLATION_TARGET_ID_HEADER: "987654",
347353
},
348354
"integration_provider": "github",
349355
"region": "de",
@@ -385,8 +391,8 @@ def test_forwards_conditionally_to_some_regions(self):
385391
self.forwarder.forward_if_applicable(
386392
event,
387393
headers={
388-
"HTTP_X_GITHUB_EVENT": "pull_request",
389-
"HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID": "987654",
394+
GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request",
395+
GITHUB_INSTALLATION_TARGET_ID_HEADER: "987654",
390396
},
391397
)
392398

@@ -407,8 +413,8 @@ def test_forwards_conditionally_to_some_regions(self):
407413
],
408414
"webhook_body": event,
409415
"webhook_headers": {
410-
"HTTP_X_GITHUB_EVENT": "pull_request",
411-
"HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID": "987654",
416+
GITHUB_WEBHOOK_TYPE_HEADER_KEY: "pull_request",
417+
GITHUB_INSTALLATION_TARGET_ID_HEADER: "987654",
412418
},
413419
"integration_provider": "github",
414420
"region": "us",

0 commit comments

Comments
 (0)