|
8 | 8 |
|
9 | 9 | from sentry.notifications.platform.service import NotificationService |
10 | 10 | from sentry.sentry_apps.api.serializers.app_platform_event import AppPlatformEvent |
| 11 | +from sentry.sentry_apps.models.sentry_app import MASKED_VALUE |
11 | 12 | from sentry.sentry_apps.utils.webhooks import IssueActionType, SentryAppResourceType |
12 | | -from sentry.shared_integrations.exceptions import ApiHostError |
| 13 | +from sentry.shared_integrations.exceptions import ApiHostError, ClientError |
13 | 14 | from sentry.testutils.asserts import assert_failure_metric |
14 | 15 | from sentry.testutils.cases import TestCase |
15 | 16 | from sentry.testutils.helpers.features import with_feature |
16 | 17 | from sentry.testutils.helpers.options import override_options |
17 | 18 | from sentry.testutils.silo import cell_silo_test |
18 | 19 | from sentry.utils import redis |
19 | 20 | from sentry.utils.circuit_breaker2 import CircuitBreaker |
| 21 | +from sentry.utils.sentry_apps import SentryAppWebhookRequestsBuffer |
20 | 22 | from sentry.utils.sentry_apps.webhooks import WebhookTimeoutError, send_and_save_webhook_request |
21 | 23 |
|
22 | 24 |
|
@@ -145,6 +147,85 @@ def test_http_error_response_records_success_and_raises( |
145 | 147 |
|
146 | 148 | mock_record_success.assert_called_once() |
147 | 149 |
|
| 150 | + @with_feature("organizations:sentry-apps-custom-webhook-headers") |
| 151 | + @override_options(CIRCUIT_BREAKER_OPTIONS) |
| 152 | + @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen") |
| 153 | + def test_error_response_buffers_masked_custom_headers(self, mock_safe_urlopen): |
| 154 | + """A failed delivery records masked custom headers in the request buffer, so the |
| 155 | + debug UI shows which custom headers were sent without persisting their secrets.""" |
| 156 | + sentry_app = self.create_sentry_app( |
| 157 | + name="HeaderApp", |
| 158 | + organization=self.organization, |
| 159 | + webhook_url="https://example.com/webhook", |
| 160 | + published=True, |
| 161 | + webhook_headers=["Authorization: Bearer super-secret"], |
| 162 | + ) |
| 163 | + install = self.create_sentry_app_installation( |
| 164 | + organization=self.organization, slug=sentry_app.slug |
| 165 | + ) |
| 166 | + event = AppPlatformEvent( |
| 167 | + resource=SentryAppResourceType.ISSUE, |
| 168 | + action=IssueActionType.CREATED, |
| 169 | + install=install, |
| 170 | + data={"test": "data"}, |
| 171 | + ) |
| 172 | + mock_safe_urlopen.return_value = _MockResponse( |
| 173 | + {}, "{}", "", False, 401, _raise_status_false, None |
| 174 | + ) |
| 175 | + |
| 176 | + with pytest.raises(ClientError): |
| 177 | + send_and_save_webhook_request(sentry_app, event) |
| 178 | + |
| 179 | + requests = SentryAppWebhookRequestsBuffer(sentry_app).get_requests(errors_only=True) |
| 180 | + assert len(requests) == 1 |
| 181 | + headers = requests[0].get("request_headers") |
| 182 | + assert headers is not None |
| 183 | + # The custom header name is recorded but its value is masked. |
| 184 | + assert headers["Authorization"] == MASKED_VALUE |
| 185 | + assert "Bearer super-secret" not in headers.values() |
| 186 | + # Sentry's own headers are still recorded in the clear. |
| 187 | + assert headers["Content-Type"] == "application/json" |
| 188 | + |
| 189 | + @override_options(CIRCUIT_BREAKER_OPTIONS) |
| 190 | + @patch("sentry.utils.sentry_apps.webhooks.safe_urlopen") |
| 191 | + def test_custom_headers_not_sent_or_logged_without_flag(self, mock_safe_urlopen): |
| 192 | + """Without the feature flag, custom headers are stripped from both the request |
| 193 | + and the buffer log.""" |
| 194 | + sentry_app = self.create_sentry_app( |
| 195 | + name="HeaderApp", |
| 196 | + organization=self.organization, |
| 197 | + webhook_url="https://example.com/webhook", |
| 198 | + published=True, |
| 199 | + webhook_headers=["Authorization: Bearer super-secret"], |
| 200 | + ) |
| 201 | + install = self.create_sentry_app_installation( |
| 202 | + organization=self.organization, slug=sentry_app.slug |
| 203 | + ) |
| 204 | + event = AppPlatformEvent( |
| 205 | + resource=SentryAppResourceType.ISSUE, |
| 206 | + action=IssueActionType.CREATED, |
| 207 | + install=install, |
| 208 | + data={"test": "data"}, |
| 209 | + ) |
| 210 | + mock_safe_urlopen.return_value = _MockResponse( |
| 211 | + {}, "{}", "", False, 401, _raise_status_false, None |
| 212 | + ) |
| 213 | + |
| 214 | + with pytest.raises(ClientError): |
| 215 | + send_and_save_webhook_request(sentry_app, event) |
| 216 | + |
| 217 | + # Custom header must not appear in the outbound request. |
| 218 | + call_headers = mock_safe_urlopen.call_args.kwargs["headers"] |
| 219 | + assert "Authorization" not in call_headers |
| 220 | + |
| 221 | + # Custom header must not appear in the buffer log either. |
| 222 | + requests = SentryAppWebhookRequestsBuffer(sentry_app).get_requests(errors_only=True) |
| 223 | + assert len(requests) == 1 |
| 224 | + headers = requests[0].get("request_headers") |
| 225 | + assert headers is not None |
| 226 | + assert "Authorization" not in headers |
| 227 | + assert headers["Content-Type"] == "application/json" |
| 228 | + |
148 | 229 |
|
149 | 230 | @cell_silo_test |
150 | 231 | class WebhookCircuitBreakerNotifyTest(TestCase): |
|
0 commit comments