diff --git a/src/sentry/hybridcloud/outbox/category.py b/src/sentry/hybridcloud/outbox/category.py index 469caf0832f7..b440ff516965 100644 --- a/src/sentry/hybridcloud/outbox/category.py +++ b/src/sentry/hybridcloud/outbox/category.py @@ -63,6 +63,7 @@ class OutboxCategory(IntEnum): SERVICE_HOOK_UPDATE = 40 SENTRY_APP_DELETE = 41 SENTRY_APP_INSTALLATION_DELETE = 42 + IDENTITY_UPDATE = 43 @classmethod def as_choices(cls) -> Sequence[tuple[int, int]]: @@ -282,6 +283,7 @@ class OutboxScope(IntEnum): OutboxCategory.UNUSED_TWO, OutboxCategory.UNUSUED_THREE, OutboxCategory.AUTH_IDENTITY_UPDATE, + OutboxCategory.IDENTITY_UPDATE, }, ) # Webhook scope is no longer in use diff --git a/src/sentry/receivers/outbox/control.py b/src/sentry/receivers/outbox/control.py index 3f90cb1d7151..abbd6449d258 100644 --- a/src/sentry/receivers/outbox/control.py +++ b/src/sentry/receivers/outbox/control.py @@ -26,6 +26,7 @@ from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation from sentry.sentry_apps.services.hook.service import hook_service from sentry.sentry_apps.tasks.sentry_apps import clear_region_cache +from sentry.users.models.identity import Identity from sentry.workflow_engine.service.action.service import action_service logger = logging.getLogger(__name__) @@ -42,6 +43,11 @@ def process_integration_updates(object_identifier: int, region_name: str, **kwds integration # Currently we do not sync any other integration changes, but if we did, you can use this variable. +@receiver(process_control_outbox, sender=OutboxCategory.IDENTITY_UPDATE) +def process_identity_updates(object_identifier: int, region_name: str, **kwds: Any): + maybe_process_tombstone(Identity, object_identifier, region_name=region_name) + + @receiver(process_control_outbox, sender=OutboxCategory.SENTRY_APP_UPDATE) def process_sentry_app_updates(object_identifier: int, region_name: str, **kwds: Any): if ( diff --git a/src/sentry/users/models/identity.py b/src/sentry/users/models/identity.py index e5fb728b0725..ea3c81523064 100644 --- a/src/sentry/users/models/identity.py +++ b/src/sentry/users/models/identity.py @@ -6,7 +6,7 @@ from django.conf import settings from django.contrib.postgres.fields.array import ArrayField -from django.db import IntegrityError, models +from django.db import IntegrityError, models, router, transaction from django.db.models import Q, QuerySet from django.utils import timezone @@ -19,7 +19,10 @@ control_silo_model, ) from sentry.db.models.manager.base import BaseManager +from sentry.hybridcloud.models.outbox import ControlOutbox, outbox_context +from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope from sentry.integrations.types import ExternalProviders, IntegrationProviderSlug +from sentry.types.region import find_all_region_names from sentry.users.services.user import RpcUser if TYPE_CHECKING: @@ -212,6 +215,20 @@ class Meta: db_table = "sentry_identity" unique_together = (("idp", "external_id"), ("idp", "user")) + def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]: + with outbox_context(transaction.atomic(router.db_for_write(Identity))): + # Fan out to all regions to ensure HybridCloudForeignKey cascade works even without org memberships + region_names = find_all_region_names() + for region_name in region_names: + ControlOutbox( + shard_scope=OutboxScope.USER_SCOPE, + shard_identifier=self.user_id, + object_identifier=self.id, + category=OutboxCategory.IDENTITY_UPDATE, + region_name=region_name, + ).save() + return super().delete(*args, **kwargs) + def get_provider(self) -> Provider: from sentry.identity import get diff --git a/tests/sentry/receivers/outbox/test_control.py b/tests/sentry/receivers/outbox/test_control.py index 65987f7cba0d..be88eab3955f 100644 --- a/tests/sentry/receivers/outbox/test_control.py +++ b/tests/sentry/receivers/outbox/test_control.py @@ -4,12 +4,14 @@ from sentry.models.apiapplication import ApiApplication from sentry.receivers.outbox.control import ( process_api_application_updates, + process_identity_updates, process_integration_updates, process_sentry_app_updates, ) from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test from sentry.types.region import Region, RegionCategory +from sentry.users.models.identity import Identity _TEST_REGION = Region("eu", 1, "http://eu.testserver", RegionCategory.MULTI_TENANT) @@ -27,6 +29,13 @@ def test_process_integration_updates(self, mock_maybe_process: MagicMock) -> Non Integration, self.identifier, region_name=_TEST_REGION.name ) + @patch("sentry.receivers.outbox.control.maybe_process_tombstone") + def test_process_identity_updates(self, mock_maybe_process: MagicMock) -> None: + process_identity_updates(object_identifier=self.identifier, region_name=_TEST_REGION.name) + mock_maybe_process.assert_called_with( + Identity, self.identifier, region_name=_TEST_REGION.name + ) + @patch("sentry.receivers.outbox.control.maybe_process_tombstone") def test_process_api_application_updates(self, mock_maybe_process: MagicMock) -> None: process_api_application_updates(