|
| 1 | +import hashlib |
| 2 | +import hmac |
1 | 3 | import ipaddress
|
2 | 4 | import logging
|
3 | 5 | from abc import ABC
|
|
15 | 17 | from sentry.api.api_owners import ApiOwner
|
16 | 18 | from sentry.api.api_publish_status import ApiPublishStatus
|
17 | 19 | from sentry.api.base import Endpoint, region_silo_endpoint
|
| 20 | +from sentry.api.exceptions import SentryAPIException |
18 | 21 | from sentry.integrations.base import IntegrationDomain
|
19 | 22 | from sentry.integrations.bitbucket.constants import BITBUCKET_IP_RANGES, BITBUCKET_IPS
|
| 23 | +from sentry.integrations.services.integration.service import integration_service |
20 | 24 | from sentry.integrations.source_code_management.webhook import SCMWebhook
|
21 | 25 | from sentry.integrations.utils.metrics import IntegrationWebhookEvent, IntegrationWebhookEventType
|
22 | 26 | from sentry.models.commit import Commit
|
|
31 | 35 | PROVIDER_NAME = "integrations:bitbucket"
|
32 | 36 |
|
33 | 37 |
|
| 38 | +def is_valid_signature(body: bytes, secret: str, signature: str) -> bool: |
| 39 | + hash_object = hmac.new( |
| 40 | + secret.encode("utf-8"), |
| 41 | + msg=body, |
| 42 | + digestmod=hashlib.sha256, |
| 43 | + ) |
| 44 | + expected_signature = hash_object.hexdigest() |
| 45 | + |
| 46 | + if not hmac.compare_digest(expected_signature, signature): |
| 47 | + logger.info( |
| 48 | + "%s.webhook.invalid-signature", |
| 49 | + PROVIDER_NAME, |
| 50 | + extra={"expected": expected_signature, "given": signature}, |
| 51 | + ) |
| 52 | + return False |
| 53 | + return True |
| 54 | + |
| 55 | + |
| 56 | +class WebhookMissingSignatureException(SentryAPIException): |
| 57 | + status_code = 400 |
| 58 | + code = f"{PROVIDER_NAME}.webhook.missing-signature" |
| 59 | + message = "Missing webhook signature" |
| 60 | + |
| 61 | + |
| 62 | +class WebhookUnsupportedSignatureMethodException(SentryAPIException): |
| 63 | + status_code = 400 |
| 64 | + code = f"{PROVIDER_NAME}.webhook.unsupported-signature-method" |
| 65 | + message = "Signature method is not supported" |
| 66 | + |
| 67 | + |
| 68 | +class WebhookInvalidSignatureException(SentryAPIException): |
| 69 | + status_code = 400 |
| 70 | + code = f"{PROVIDER_NAME}.webhook.invalid-signature" |
| 71 | + message = "Webhook signature is invalid" |
| 72 | + |
| 73 | + |
34 | 74 | class BitbucketWebhook(SCMWebhook, ABC):
|
35 | 75 | @property
|
36 | 76 | def provider(self) -> str:
|
@@ -74,18 +114,11 @@ def event_type(self) -> IntegrationWebhookEventType:
|
74 | 114 | def __call__(self, event: Mapping[str, Any], **kwargs) -> None:
|
75 | 115 | authors = {}
|
76 | 116 |
|
| 117 | + if not (repo := kwargs.get("repo")): |
| 118 | + raise ValueError("Missing repo") |
77 | 119 | if not (organization := kwargs.get("organization")):
|
78 | 120 | raise ValueError("Missing organization")
|
79 | 121 |
|
80 |
| - try: |
81 |
| - repo = Repository.objects.get( |
82 |
| - organization_id=organization.id, |
83 |
| - provider=PROVIDER_NAME, |
84 |
| - external_id=str(event["repository"]["uuid"]), |
85 |
| - ) |
86 |
| - except Repository.DoesNotExist: |
87 |
| - raise Http404() |
88 |
| - |
89 | 122 | # while we're here, make sure repo data is up to date
|
90 | 123 | self.update_repo_data(repo, event)
|
91 | 124 |
|
@@ -199,13 +232,36 @@ def post(self, request: HttpRequest, organization_id: int) -> HttpResponse:
|
199 | 232 | )
|
200 | 233 | return HttpResponse(status=400)
|
201 | 234 |
|
| 235 | + try: |
| 236 | + repo = Repository.objects.get( |
| 237 | + organization_id=organization.id, |
| 238 | + provider=PROVIDER_NAME, |
| 239 | + external_id=str(event["repository"]["uuid"]), |
| 240 | + ) |
| 241 | + except Repository.DoesNotExist: |
| 242 | + raise Http404() |
| 243 | + |
| 244 | + integration = integration_service.get_integration(integration_id=repo.integration_id) |
| 245 | + if integration and "webhook_secret" in integration.metadata: |
| 246 | + secret = integration.metadata["webhook_secret"] |
| 247 | + try: |
| 248 | + method, signature = request.META["HTTP_X_HUB_SIGNATURE"].split("=", 1) |
| 249 | + except (IndexError, KeyError, ValueError): |
| 250 | + raise WebhookMissingSignatureException() |
| 251 | + |
| 252 | + if method != "sha256": |
| 253 | + raise WebhookUnsupportedSignatureMethodException() |
| 254 | + |
| 255 | + if not is_valid_signature(request.body, secret, signature): |
| 256 | + raise WebhookInvalidSignatureException() |
| 257 | + |
202 | 258 | event_handler = handler()
|
203 | 259 |
|
204 | 260 | with IntegrationWebhookEvent(
|
205 | 261 | interaction_type=event_handler.event_type,
|
206 | 262 | domain=IntegrationDomain.SOURCE_CODE_MANAGEMENT,
|
207 | 263 | provider_key=event_handler.provider,
|
208 | 264 | ).capture():
|
209 |
| - event_handler(event, organization=organization) |
| 265 | + event_handler(event, repo=repo, organization=organization) |
210 | 266 |
|
211 | 267 | return HttpResponse(status=204)
|
0 commit comments