Browse Source

feat(hybrid-cloud): Update and mark Vercel's webhook endpoints as control silo only (#51115)

Alberto Leal 1 year ago
parent
commit
4e91598a34

+ 30 - 16
src/sentry/integrations/vercel/webhook.py

@@ -14,13 +14,13 @@ from sentry import VERSION, audit_log, http, options
 from sentry.api.base import Endpoint, control_silo_endpoint
 from sentry.models import (
     Integration,
-    Organization,
     OrganizationIntegration,
     Project,
     SentryAppInstallationForProvider,
     SentryAppInstallationToken,
 )
 from sentry.services.hybrid_cloud.organization import organization_service
+from sentry.services.hybrid_cloud.project import project_service
 from sentry.shared_integrations.exceptions import IntegrationError
 from sentry.utils.audit import create_audit_entry
 from sentry.utils.http import absolute_uri
@@ -93,7 +93,9 @@ def get_payload_and_token(
     meta = payload["deployment"]["meta"]
 
     # look up the project so we can get the slug
-    project = Project.objects.get(id=sentry_project_id)
+    project = project_service.get_by_id(organization_id=organization_id, id=sentry_project_id)
+    if project is None:
+        raise Project.DoesNotExist
 
     # find the connected sentry app installation
     installation_for_provider = SentryAppInstallationForProvider.objects.select_related(
@@ -131,6 +133,27 @@ class VercelWebhookEndpoint(Endpoint):
     def dispatch(self, request: Request, *args, **kwargs) -> Response:
         return super().dispatch(request, *args, **kwargs)
 
+    def parse_new_external_id(self, request: Request) -> str:
+        payload = request.data["payload"]
+        # New Vercel request flow
+        external_id = (
+            payload.get("team")["id"]
+            if (payload.get("team") and payload.get("team") != {})
+            else payload["user"]["id"]
+        )
+        return external_id
+
+    def parse_old_external_id(self, request: Request) -> str:
+        # Old Vercel request flow
+        external_id = request.data.get("teamId") or request.data["userId"]
+        return external_id
+
+    def parse_external_id(self, request: Request) -> str:
+        try:
+            return self.parse_new_external_id(request)
+        except Exception:
+            return self.parse_old_external_id(request)
+
     def post(self, request: Request) -> Response:
         if not request.META.get("HTTP_X_VERCEL_SIGNATURE"):
             logger.error("vercel.webhook.missing-signature")
@@ -156,11 +179,7 @@ class VercelWebhookEndpoint(Endpoint):
             # Try the new Vercel request. If it fails, try the old Vercel request
             try:
                 payload = request.data["payload"]
-                external_id = (
-                    payload.get("team")["id"]
-                    if (payload.get("team") and payload.get("team") != {})
-                    else payload["user"]["id"]
-                )
+                external_id = self.parse_new_external_id(request)
                 scope.set_tag("vercel_webhook.type", "new")
 
                 if event_type == "integration-configuration.removed":
@@ -169,7 +188,7 @@ class VercelWebhookEndpoint(Endpoint):
                 if event_type == "deployment.created":
                     return self._deployment_created(external_id, request)
             except Exception:
-                external_id = request.data.get("teamId") or request.data["userId"]
+                external_id = self.parse_old_external_id(request)
                 scope.set_tag("vercel_webhook.type", "old")
 
                 if event_type == "integration-configuration-removed":
@@ -184,15 +203,11 @@ class VercelWebhookEndpoint(Endpoint):
             # Try the new Vercel request. If it fails, try the old Vercel request
             try:
                 payload = request.data["payload"]
-                external_id = (
-                    payload.get("team")["id"]
-                    if (payload.get("team") and payload.get("team") != {})
-                    else payload["user"]["id"]
-                )
+                external_id = self.parse_new_external_id(request)
                 scope.set_tag("vercel_webhook.type", "new")
                 configuration_id = payload["configuration"]["id"]
             except Exception:
-                external_id = request.data.get("teamId") or request.data["userId"]
+                external_id = self.parse_old_external_id(request)
                 scope.set_tag("vercel_webhook.type", "old")
                 configuration_id = request.data.get("configurationId")
 
@@ -275,10 +290,9 @@ class VercelWebhookEndpoint(Endpoint):
                 organization_id=configuration["organization_id"], integration_id=integration.id
             ).delete()
 
-            organization = Organization.objects.get(id=configuration["organization_id"])
             create_audit_entry(
                 request=request,
-                organization=organization,
+                organization_id=configuration["organization_id"],
                 target_object=integration.id,
                 event=audit_log.get_event_id("INTEGRATION_REMOVE"),
                 actor_label="Vercel User",

+ 12 - 0
src/sentry/services/hybrid_cloud/project/impl.py

@@ -3,9 +3,21 @@ from __future__ import annotations
 from sentry.models import Project, ProjectOption
 from sentry.services.hybrid_cloud import OptionValue
 from sentry.services.hybrid_cloud.project import ProjectService, RpcProject, RpcProjectOptionValue
+from sentry.services.hybrid_cloud.project.serial import serialize_project
 
 
 class DatabaseBackedProjectService(ProjectService):
+    def get_by_id(self, *, organization_id: int, id: int) -> RpcProject | None:
+        try:
+            project = Project.objects.get_from_cache(id=id, organization=organization_id)
+        except ValueError:
+            project = Project.objects.filter(id=id, organization=organization_id).first()
+        except Project.DoesNotExist:
+            return None
+        if project:
+            return serialize_project(project)
+        return None
+
     def get_option(self, *, project: RpcProject, key: str) -> RpcProjectOptionValue:
         from sentry import projectoptions
 

+ 7 - 2
src/sentry/services/hybrid_cloud/project/service.py

@@ -4,11 +4,11 @@
 # defined, because we want to reflect on type annotations and avoid forward references.
 
 from abc import abstractmethod
-from typing import cast
+from typing import Optional, cast
 
 from sentry.services.hybrid_cloud import OptionValue
 from sentry.services.hybrid_cloud.project import RpcProject, RpcProjectOptionValue
-from sentry.services.hybrid_cloud.region import ByOrganizationIdAttribute
+from sentry.services.hybrid_cloud.region import ByOrganizationId, ByOrganizationIdAttribute
 from sentry.services.hybrid_cloud.rpc import RpcService, regional_rpc_method
 from sentry.silo import SiloMode
 
@@ -38,5 +38,10 @@ class ProjectService(RpcService):
     def delete_option(self, *, project: RpcProject, key: str) -> None:
         pass
 
+    @regional_rpc_method(resolve=ByOrganizationId())
+    @abstractmethod
+    def get_by_id(self, *, organization_id: int, id: int) -> Optional[RpcProject]:
+        pass
+
 
 project_service = cast(ProjectService, ProjectService.create_delegation())

+ 6 - 3
tests/sentry/integrations/vercel/test_webhook.py

@@ -24,11 +24,12 @@ from sentry.models import (
 )
 from sentry.testutils import APITestCase
 from sentry.testutils.helpers import override_options
-from sentry.testutils.silo import control_silo_test
+from sentry.testutils.silo import control_silo_test, exempt_from_silo_limits
 from sentry.utils import json
 from sentry.utils.http import absolute_uri
 
 
+@control_silo_test(stable=True)
 class SignatureVercelTest(APITestCase):
     webhook_url = "/extensions/vercel/webhook/"
 
@@ -48,7 +49,7 @@ class SignatureVercelTest(APITestCase):
             assert response.status_code == 401
 
 
-@control_silo_test
+@control_silo_test(stable=True)
 class VercelReleasesTest(APITestCase):
     webhook_url = "/extensions/vercel/webhook/"
     header = "VERCEL"
@@ -210,7 +211,8 @@ class VercelReleasesTest(APITestCase):
             absolute_uri("/api/0/organizations/%s/releases/" % self.organization.slug),
             json={},
         )
-        self.project.delete()
+        with exempt_from_silo_limits():
+            self.project.delete()
 
         with override_options({"vercel.client-secret": SECRET}):
             response = self._get_response(EXAMPLE_DEPLOYMENT_WEBHOOK_RESPONSE_OLD, SIGNATURE)
@@ -308,6 +310,7 @@ class VercelReleasesTest(APITestCase):
         assert "Could not determine repository" == response.data["detail"]
 
 
+@control_silo_test(stable=True)
 class VercelReleasesNewTest(VercelReleasesTest):
     webhook_url = "/extensions/vercel/delete/"
     header = "VERCEL"