Repository: getsentry/sentry

Issue:
Prevent deleting org if org has published 3rd party integration
If an organization has a published 3rd party integration, we should prevent that org from being deleted.


Discussion:

Resolving patch:
diff --git a/src/sentry/api/endpoints/organization_details.py b/src/sentry/api/endpoints/organization_details.py
--- a/src/sentry/api/endpoints/organization_details.py
+++ b/src/sentry/api/endpoints/organization_details.py
@@ -22,7 +22,7 @@
     TrustedRelaySerializer,
 )
 from sentry.api.serializers.rest_framework import ListField
-from sentry.constants import LEGACY_RATE_LIMIT_OPTIONS
+from sentry.constants import LEGACY_RATE_LIMIT_OPTIONS, SentryAppStatus
 from sentry.datascrubbing import validate_pii_config_update
 from sentry.integrations.utils.codecov import has_codecov_integration
 from sentry.lang.native.utils import (
@@ -40,6 +40,7 @@
     ScheduledDeletion,
     UserEmail,
 )
+from sentry.models.integrations import SentryApp
 from sentry.services.hybrid_cloud import IDEMPOTENCY_KEY_LENGTH
 from sentry.services.hybrid_cloud.organization_actions.impl import (
     mark_organization_as_pending_deletion_with_outbox_message,
@@ -51,6 +52,7 @@
 ERR_NO_2FA = "Cannot require two-factor authentication without personal two-factor enabled."
 ERR_SSO_ENABLED = "Cannot require two-factor authentication with SSO enabled"
 ERR_EMAIL_VERIFICATION = "Cannot require email verification before verifying your email address."
+ERR_3RD_PARTY_PUBLISHED_APP = "Cannot delete an organization that owns a published integration."
 
 ORG_OPTIONS = (
     # serializer field name, option key name, type, default value
@@ -566,6 +568,10 @@ def handle_delete(self, request: Request, organization):
             return self.respond({"detail": ERR_NO_USER}, status=401)
         if organization.is_default:
             return self.respond({"detail": ERR_DEFAULT_ORG}, status=400)
+        if SentryApp.objects.filter(
+            owner_id=organization.id, status=SentryAppStatus.PUBLISHED
+        ).exists():
+            return self.respond({"detail": ERR_3RD_PARTY_PUBLISHED_APP}, status=400)
 
         with transaction.atomic():
             updated_organization = mark_organization_as_pending_deletion_with_outbox_message(


Test patch:
diff --git a/tests/sentry/api/endpoints/test_organization_details.py b/tests/sentry/api/endpoints/test_organization_details.py
--- a/tests/sentry/api/endpoints/test_organization_details.py
+++ b/tests/sentry/api/endpoints/test_organization_details.py
@@ -917,6 +917,17 @@ def test_organization_does_not_exist(self):
 
         self.get_error_response("nonexistent-slug", status_code=404)
 
+    def test_published_sentry_app(self):
+        """Test that we do not allow an organization who has a published sentry app to be deleted"""
+        org = self.create_organization(name="test", owner=self.user)
+        self.create_sentry_app(
+            organization=org,
+            scopes=["project:write"],
+            published=True,
+        )
+        self.login_as(self.user)
+        self.get_error_response(org.slug, status_code=400)
+
 
 @region_silo_test
 class OrganizationSettings2FATest(TwoFactorAPITestCase):