Browse Source

chore(hybrid-cloud): More query changes in support for breaking foreign (#46365)

Extracting more query changes from
https://github.com/getsentry/sentry/pull/45528/files
Vast majority of these are test related changes.  More to come.
Zach Collins 1 year ago
parent
commit
3e8de25949

+ 4 - 1
src/sentry/api/endpoints/team_notification_settings_details.py

@@ -8,6 +8,7 @@ from sentry.api.serializers import serialize
 from sentry.api.serializers.models.notification_setting import NotificationSettingsSerializer
 from sentry.api.validators.notifications import validate, validate_type_option
 from sentry.models import NotificationSetting, Team
+from sentry.services.hybrid_cloud.actor import RpcActor
 
 
 @region_silo_endpoint
@@ -68,6 +69,8 @@ class TeamNotificationSettingsDetailsEndpoint(TeamEndpoint):
         """
 
         notification_settings = validate(request.data, team=team)
-        NotificationSetting.objects.update_settings_bulk(notification_settings, team=team)
+        NotificationSetting.objects.update_settings_bulk(
+            notification_settings, actor=RpcActor.from_orm_team(team)
+        )
 
         return Response(status=status.HTTP_204_NO_CONTENT)

+ 4 - 1
src/sentry/api/endpoints/user_notification_settings_details.py

@@ -9,6 +9,7 @@ from sentry.api.serializers.models.notification_setting import NotificationSetti
 from sentry.api.validators.notifications import validate, validate_type_option
 from sentry.models import NotificationSetting, User
 from sentry.notifications.helpers import get_providers_for_recipient
+from sentry.services.hybrid_cloud.actor import RpcActor
 
 
 @control_silo_endpoint
@@ -88,6 +89,8 @@ class UserNotificationSettingsDetailsEndpoint(UserEndpoint):
         """
 
         notification_settings = validate(request.data, user=user)
-        NotificationSetting.objects.update_settings_bulk(notification_settings, user=user)
+        NotificationSetting.objects.update_settings_bulk(
+            notification_settings, actor=RpcActor.from_orm_user(user)
+        )
 
         return Response(status=status.HTTP_204_NO_CONTENT)

+ 11 - 7
src/sentry/auth/access.py

@@ -625,19 +625,23 @@ class OrganizationMemberAccess(DbAccess):
 class OrganizationGlobalAccess(DbAccess):
     """Access to all an organization's teams and projects."""
 
-    def __init__(self, organization: Organization, scopes: Iterable[str], **kwargs: Any) -> None:
-        self._organization = organization
+    def __init__(
+        self, organization: Organization | int, scopes: Iterable[str], **kwargs: Any
+    ) -> None:
+        self._organization_id = (
+            organization.id if isinstance(organization, Organization) else organization
+        )
 
         super().__init__(has_global_access=True, scopes=frozenset(scopes), **kwargs)
 
     def has_team_access(self, team: Team) -> bool:
         return bool(
-            team.organization_id == self._organization.id and team.status == TeamStatus.VISIBLE
+            team.organization_id == self._organization_id and team.status == TeamStatus.VISIBLE
         )
 
     def has_project_access(self, project: Project) -> bool:
         return bool(
-            project.organization_id == self._organization.id
+            project.organization_id == self._organization_id
             and project.status == ProjectStatus.VISIBLE
         )
 
@@ -645,7 +649,7 @@ class OrganizationGlobalAccess(DbAccess):
     def accessible_team_ids(self) -> FrozenSet[int]:
         return frozenset(
             Team.objects.filter(
-                organization=self._organization, status=TeamStatus.VISIBLE
+                organization_id=self._organization_id, status=TeamStatus.VISIBLE
             ).values_list("id", flat=True)
         )
 
@@ -653,7 +657,7 @@ class OrganizationGlobalAccess(DbAccess):
     def accessible_project_ids(self) -> FrozenSet[int]:
         return frozenset(
             Project.objects.filter(
-                organization=self._organization, status=ProjectStatus.VISIBLE
+                organization_id=self._organization_id, status=ProjectStatus.VISIBLE
             ).values_list("id", flat=True)
         )
 
@@ -1113,7 +1117,7 @@ def from_auth(auth: ApiKey | SystemToken, organization: Organization) -> Access:
     assert not isinstance(auth, SystemToken)
     if auth.organization_id == organization.id:
         return OrganizationGlobalAccess(
-            auth.organization, settings.SENTRY_SCOPES, sso_is_valid=True
+            auth.organization_id, settings.SENTRY_SCOPES, sso_is_valid=True
         )
     else:
         return DEFAULT

+ 1 - 1
src/sentry/auth/helper.py

@@ -825,7 +825,7 @@ class AuthHelper(Pipeline):
         self.disable_2fa_required()
 
         self.provider_model = AuthProvider.objects.create(
-            organization=self.organization, provider=self.provider.key, config=config
+            organization_id=self.organization.id, provider=self.provider.key, config=config
         )
 
         self.auth_handler(identity).handle_attach_identity(om)

+ 37 - 24
src/sentry/integrations/slack/views/link_team.py

@@ -2,20 +2,16 @@ from typing import Any, Sequence
 
 from django import forms
 from django.core.signing import BadSignature, SignatureExpired
-from django.http import HttpResponse
+from django.http import Http404, HttpResponse
 from rest_framework.request import Request
 
 from sentry import analytics
-from sentry.models import (
-    ExternalActor,
-    Identity,
-    IdentityProvider,
-    Integration,
-    NotificationSetting,
-    OrganizationMember,
-    Team,
-)
+from sentry.models import ExternalActor, Integration, OrganizationMember, Team
 from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes
+from sentry.services.hybrid_cloud.actor import RpcActor
+from sentry.services.hybrid_cloud.identity import identity_service
+from sentry.services.hybrid_cloud.integration import integration_service
+from sentry.services.hybrid_cloud.notifications import notifications_service
 from sentry.types.integrations import ExternalProviders
 from sentry.utils.signing import unsign
 from sentry.web.decorators import transaction_start
@@ -74,7 +70,10 @@ class SlackLinkTeamView(BaseView):
                 request=request,
             )
 
-        integration = Integration.objects.get(id=params["integration_id"])
+        integration = integration_service.get_integration(integration_id=params["integration_id"])
+        if integration is None:
+            raise Http404
+
         organization_memberships = OrganizationMember.objects.get_for_integration(
             integration, request.user
         )
@@ -114,20 +113,23 @@ class SlackLinkTeamView(BaseView):
         if not team:
             return render_error_page(request, body_text="HTTP 404: Team does not exist")
 
-        try:
-            idp = IdentityProvider.objects.get(type="slack", external_id=integration.external_id)
-        except IdentityProvider.DoesNotExist:
+        idp = identity_service.get_provider(
+            provider_type="slack", provider_ext_id=integration.external_id
+        )
+        if idp is None:
             logger.info("slack.action.invalid-team-id", extra={"slack_id": integration.external_id})
             return render_error_page(request, body_text="HTTP 403: Invalid team ID")
 
-        if not Identity.objects.filter(idp=idp, external_id=params["slack_id"]).exists():
+        ident = identity_service.get_identity(
+            provider_id=idp.id, identity_ext_id=params["slack_id"]
+        )
+        if not ident:
             return render_error_page(request, body_text="HTTP 403: User identity does not exist")
 
-        install = integration.get_installation(team.organization.id)
         external_team, created = ExternalActor.objects.get_or_create(
             actor_id=team.actor_id,
             organization=team.organization,
-            integration=integration,
+            integration_id=integration.id,
             provider=ExternalProviders.SLACK.value,
             defaults=dict(
                 external_name=channel_name,
@@ -144,7 +146,13 @@ class SlackLinkTeamView(BaseView):
 
         if not created:
             message = ALREADY_LINKED_MESSAGE.format(slug=team.slug)
-            install.send_message(channel_id=channel_id, message=message)
+
+            integration_service.send_message(
+                integration_id=integration.id,
+                organization_id=team.organization_id,
+                channel=channel_id,
+                message=message,
+            )
             return render_to_response(
                 "sentry/integrations/slack/post-linked-team.html",
                 request=request,
@@ -157,14 +165,19 @@ class SlackLinkTeamView(BaseView):
             )
 
         # Turn on notifications for all of a team's projects.
-        NotificationSetting.objects.update_settings(
-            ExternalProviders.SLACK,
-            NotificationSettingTypes.ISSUE_ALERTS,
-            NotificationSettingOptionValues.ALWAYS,
-            team=team,
+        notifications_service.update_settings(
+            external_provider=ExternalProviders.SLACK,
+            notification_type=NotificationSettingTypes.ISSUE_ALERTS,
+            setting_option=NotificationSettingOptionValues.ALWAYS,
+            actor=RpcActor.from_orm_team(team),
         )
         message = SUCCESS_LINKED_MESSAGE.format(slug=team.slug, channel_name=channel_name)
-        install.send_message(channel_id=channel_id, message=message)
+        integration_service.send_message(
+            integration_id=integration.id,
+            organization_id=team.organization_id,
+            channel=channel_id,
+            message=message,
+        )
         return render_to_response(
             "sentry/integrations/slack/post-linked-team.html",
             request=request,

+ 1 - 1
src/sentry/integrations/slack/views/unlink_identity.py

@@ -57,7 +57,7 @@ class SlackUnlinkIdentityView(BaseView):
             )
 
         try:
-            Identity.objects.filter(idp=idp, external_id=params["slack_id"]).delete()
+            Identity.objects.filter(idp_id=idp.id, external_id=params["slack_id"]).delete()
         except IntegrityError:
             logger.exception("slack.unlink.integrity-error")
             raise Http404

+ 7 - 4
src/sentry/integrations/slack/views/unlink_team.py

@@ -4,7 +4,8 @@ from rest_framework.response import Response
 
 from sentry.integrations.mixins import SUCCESS_UNLINKED_TEAM_MESSAGE, SUCCESS_UNLINKED_TEAM_TITLE
 from sentry.integrations.utils import get_identity_or_404
-from sentry.models import ExternalActor, Identity, Integration
+from sentry.models import ExternalActor, Integration
+from sentry.services.hybrid_cloud.identity import identity_service
 from sentry.types.integrations import ExternalProviders
 from sentry.utils.signing import unsign
 from sentry.web.decorators import transaction_start
@@ -56,8 +57,8 @@ class SlackUnlinkTeamView(BaseView):
         channel_id = params["channel_id"]
 
         external_teams = ExternalActor.objects.filter(
-            organization=organization,
-            integration=integration,
+            organization_id=organization.id,
+            integration_id=integration.id,
             provider=ExternalProviders.SLACK.value,
             external_name=channel_name,
             external_id=channel_id,
@@ -78,7 +79,9 @@ class SlackUnlinkTeamView(BaseView):
                 },
             )
 
-        if not Identity.objects.filter(idp=idp, external_id=params["slack_id"]).exists():
+        if not identity_service.get_identity(
+            provider_id=idp.id, identity_ext_id=params["slack_id"]
+        ):
             return render_error_page(request, body_text="HTTP 403: User identity does not exist")
 
         # Someone may have accidentally added multiple teams so unlink them all.

+ 17 - 16
src/sentry/integrations/utils/identities.py

@@ -6,10 +6,12 @@ from sentry.models import (
     Identity,
     IdentityProvider,
     IdentityStatus,
-    Integration,
     Organization,
+    OrganizationMember,
     User,
 )
+from sentry.services.hybrid_cloud.identity import RpcIdentityProvider, identity_service
+from sentry.services.hybrid_cloud.integration import RpcIntegration, integration_service
 from sentry.types.integrations import EXTERNAL_PROVIDERS, ExternalProviders
 
 
@@ -18,22 +20,21 @@ def get_identity_or_404(
     user: User,
     integration_id: int,
     organization_id: Optional[int] = None,
-) -> Tuple[Organization, Integration, IdentityProvider]:
+) -> Tuple[Organization, RpcIntegration, RpcIdentityProvider]:
     """For endpoints, short-circuit with a 404 if we cannot find everything we need."""
-    try:
-        integration = Integration.objects.get(id=integration_id)
-        idp = IdentityProvider.objects.get(
-            external_id=integration.external_id, type=EXTERNAL_PROVIDERS[provider]
-        )
-        organization_filters = dict(
-            member_set__user=user,
-            organizationintegration__integration=integration,
-        )
-        # If provided, ensure organization_id is valid.
-        if organization_id:
-            organization_filters.update(dict(id=organization_id))
-        organization = Organization.objects.filter(**organization_filters)[0]
-    except Exception:
+    if provider not in EXTERNAL_PROVIDERS:
+        raise Http404
+
+    integration = integration_service.get_integration(integration_id=integration_id)
+    idp = identity_service.get_provider(
+        provider_ext_id=integration.external_id, provider_type=EXTERNAL_PROVIDERS[provider]
+    )
+
+    qs = OrganizationMember.objects.get_for_integration(integration, user)
+    if organization_id:
+        qs = qs.filter(organization_id=organization_id)
+    organization = qs.first().organization if qs else None
+    if organization is None:
         raise Http404
     return organization, integration, idp
 

+ 10 - 19
src/sentry/models/authprovider.py

@@ -13,7 +13,6 @@ from sentry.db.models import (
 )
 from sentry.db.models.fields.jsonfield import JSONField
 from sentry.models.organizationmember import OrganizationMember
-from sentry.utils.http import absolute_uri
 
 logger = logging.getLogger("sentry.authprovider")
 
@@ -89,52 +88,44 @@ class AuthProvider(Model):
 
         if self.flags.scim_enabled:
             return SentryAppInstallationToken.objects.get_token(
-                self.organization, f"{self.provider}_scim"
+                self.organization_id, f"{self.provider}_scim"
             )
         else:
             logger.warning(
                 "SCIM disabled but tried to access token",
-                extra={"organization_id": self.organization.id},
+                extra={"organization_id": self.organization_id},
             )
             return None
 
-    def get_scim_url(self):
-        if self.flags.scim_enabled:
-            # the SCIM protocol doesn't use trailing slashes in URLs
-            return absolute_uri(f"api/0/organizations/{self.organization.slug}/scim/v2")
-
-        else:
-            return None
-
     def enable_scim(self, user):
         from sentry.models import SentryAppInstallation, SentryAppInstallationForProvider
         from sentry.sentry_apps.apps import SentryAppCreator
 
         if (
-            not self.get_provider().can_use_scim(self.organization, user)
+            not self.get_provider().can_use_scim(self.organization_id, user)
             or self.flags.scim_enabled is True
         ):
             logger.warning(
                 "SCIM already enabled",
-                extra={"organization_id": self.organization.id},
+                extra={"organization_id": self.organization_id},
             )
             return
 
         # check if we have a scim app already
 
         if SentryAppInstallationForProvider.objects.filter(
-            organization=self.organization, provider="okta_scim"
+            organization=self.organization_id, provider="okta_scim"
         ).exists():
             logger.warning(
                 "SCIM installation already exists",
-                extra={"organization_id": self.organization.id},
+                extra={"organization_id": self.organization_id},
             )
             return
 
         sentry_app = SentryAppCreator(
             name="SCIM Internal Integration",
             author="Auto-generated by Sentry",
-            organization_id=self.organization.id,
+            organization_id=self.organization_id,
             overview=SCIM_INTERNAL_INTEGRATION_OVERVIEW,
             is_internal=True,
             verify_install=False,
@@ -149,14 +140,14 @@ class AuthProvider(Model):
         sentry_app_installation = SentryAppInstallation.objects.get(sentry_app=sentry_app)
         SentryAppInstallationForProvider.objects.create(
             sentry_app_installation=sentry_app_installation,
-            organization=self.organization,
+            organization_id=self.organization_id,
             provider=f"{self.provider}_scim",
         )
         self.flags.scim_enabled = True
 
     def _reset_idp_flags(self):
         OrganizationMember.objects.filter(
-            organization=self.organization,
+            organization_id=self.organization_id,
             flags=models.F("flags").bitor(OrganizationMember.flags["idp:provisioned"]),
         ).update(
             flags=models.F("flags")
@@ -170,7 +161,7 @@ class AuthProvider(Model):
 
         if self.flags.scim_enabled:
             install = SentryAppInstallationForProvider.objects.get(
-                organization=self.organization, provider=f"{self.provider}_scim"
+                organization=self.organization_id, provider=f"{self.provider}_scim"
             )
             # Only one SCIM installation allowed per organization. So we can reset the idp flags for the orgs
             # We run this update before the app is uninstalled to avoid ending up in a situation where there are

+ 10 - 7
src/sentry/models/identity.py

@@ -22,6 +22,7 @@ from sentry.types.integrations import ExternalProviders
 
 if TYPE_CHECKING:
     from sentry.models import User
+    from sentry.services.hybrid_cloud.identity import RpcIdentityProvider
 
 logger = logging.getLogger(__name__)
 
@@ -73,7 +74,7 @@ class IdentityManager(BaseManager):
     def link_identity(
         self,
         user: User,
-        idp: IdentityProvider,
+        idp: IdentityProvider | RpcIdentityProvider,
         external_id: str,
         should_reattach: bool = True,
         defaults: Mapping[str, Any | None] = None,
@@ -90,7 +91,7 @@ class IdentityManager(BaseManager):
         }
         try:
             identity, created = self.get_or_create(
-                idp=idp, user=user, external_id=external_id, defaults=defaults
+                idp_id=idp.id, user=user, external_id=external_id, defaults=defaults
             )
             if not created:
                 identity.update(**defaults)
@@ -107,8 +108,10 @@ class IdentityManager(BaseManager):
         )
         return identity
 
-    def delete_identity(self, user: User, idp: IdentityProvider, external_id: str) -> None:
-        self.filter(Q(external_id=external_id) | Q(user=user), idp=idp).delete()
+    def delete_identity(
+        self, user: User, idp: IdentityProvider | RpcIdentityProvider, external_id: str
+    ) -> None:
+        self.filter(Q(external_id=external_id) | Q(user=user), idp_id=idp.id).delete()
         logger.info(
             "deleted-identity",
             extra={"external_id": external_id, "idp_id": idp.id, "user_id": user.id},
@@ -116,12 +119,12 @@ class IdentityManager(BaseManager):
 
     def create_identity(
         self,
-        idp: IdentityProvider,
+        idp: IdentityProvider | RpcIdentityProvider,
         external_id: str,
         user: User,
         defaults: Mapping[str, Any],
     ) -> Identity:
-        identity_model = self.create(idp=idp, user=user, external_id=external_id, **defaults)
+        identity_model = self.create(idp_id=idp.id, user=user, external_id=external_id, **defaults)
         logger.info(
             "created-identity",
             extra={
@@ -135,7 +138,7 @@ class IdentityManager(BaseManager):
 
     def reattach(
         self,
-        idp: IdentityProvider,
+        idp: IdentityProvider | RpcIdentityProvider,
         external_id: str,
         user: User,
         defaults: Mapping[str, Any],

Some files were not shown because too many files changed in this diff