diff --git a/src/sentry/integrations/aws_lambda/integration.py b/src/sentry/integrations/aws_lambda/integration.py index 468b6841995c17..0f5bf170c778bd 100644 --- a/src/sentry/integrations/aws_lambda/integration.py +++ b/src/sentry/integrations/aws_lambda/integration.py @@ -20,9 +20,10 @@ IntegrationProvider, ) from sentry.integrations.mixins import ServerlessMixin -from sentry.models import Integration, OrganizationIntegration, Project +from sentry.models import Integration, OrganizationIntegration, Project, User from sentry.pipeline import PipelineView -from sentry.services.hybrid_cloud.organization import RpcOrganizationSummary +from sentry.services.hybrid_cloud.organization import RpcOrganizationSummary, organization_service +from sentry.services.hybrid_cloud.user.serial import serialize_rpc_user from sentry.utils.sdk import capture_exception from .client import ConfigurationError, gen_aws_client @@ -270,7 +271,12 @@ def dispatch(self, request: Request, pipeline) -> Response: curr_step = 0 if pipeline.fetch_state("skipped_project_select") else 1 def render_response(error=None): - serialized_organization = serialize(pipeline.organization, request.user) + serialized_organization = organization_service.serialize_organization( + id=pipeline.organization.id, + as_user=serialize_rpc_user(request.user) + if isinstance(request.user, User) + else None, + ) template_url = options.get("aws-lambda.cloudformation-url") context = { "baseCloudformationUrl": "https://console.aws.amazon.com/cloudformation/home#/stacks/create/review", diff --git a/src/sentry/pipeline/base.py b/src/sentry/pipeline/base.py index e9690a156836f7..60c25a94e972ba 100644 --- a/src/sentry/pipeline/base.py +++ b/src/sentry/pipeline/base.py @@ -16,7 +16,7 @@ from sentry.web.helpers import render_to_response from ..models import Organization -from ..services.hybrid_cloud.organization.serial import serialize_organization +from ..services.hybrid_cloud.organization.serial import serialize_rpc_organization from . import PipelineProvider from .constants import PIPELINE_STATE_TTL from .store import PipelineSessionStore @@ -104,7 +104,7 @@ def __init__( ) -> None: self.request = request self.organization: RpcOrganization | None = ( - serialize_organization(organization) + serialize_rpc_organization(organization) if isinstance(organization, Organization) else organization ) diff --git a/src/sentry/rules/actions/notify_event_service.py b/src/sentry/rules/actions/notify_event_service.py index f23c7ba55fb285..3d54c8e4e7c20b 100644 --- a/src/sentry/rules/actions/notify_event_service.py +++ b/src/sentry/rules/actions/notify_event_service.py @@ -19,7 +19,7 @@ from sentry.rules.base import CallbackFuture from sentry.services.hybrid_cloud.app import RpcSentryAppService, app_service from sentry.services.hybrid_cloud.integration import integration_service -from sentry.services.hybrid_cloud.organization.serial import serialize_organization +from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization from sentry.tasks.sentry_apps import notify_sentry_app from sentry.utils import metrics from sentry.utils.safe import safe_execute @@ -63,7 +63,7 @@ def send_incident_alert_notification( fire. If not provided we'll attempt to calculate this ourselves. :return: """ - organization = serialize_organization(incident.organization) + organization = serialize_rpc_organization(incident.organization) incident_attachment = build_incident_attachment(incident, new_status, metric_value) integration_service.send_incident_alert_notification( diff --git a/src/sentry/services/hybrid_cloud/organization/impl.py b/src/sentry/services/hybrid_cloud/organization/impl.py index e8dea54977c6c4..20d28932b4da4a 100644 --- a/src/sentry/services/hybrid_cloud/organization/impl.py +++ b/src/sentry/services/hybrid_cloud/organization/impl.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Iterable, List, Optional, Set, cast +from typing import Any, Iterable, List, Optional, Set, cast from django.db import IntegrityError, models, transaction from sentry import roles +from sentry.api.serializers import serialize from sentry.db.postgres.roles import in_test_psql_role_override from sentry.models import ( Activity, @@ -34,9 +35,10 @@ ) from sentry.services.hybrid_cloud.organization.serial import ( serialize_member, - serialize_organization, serialize_organization_summary, + serialize_rpc_organization, ) +from sentry.services.hybrid_cloud.user import RpcUser from sentry.services.hybrid_cloud.util import flags_to_bits @@ -55,6 +57,14 @@ def check_membership_by_id( return serialize_member(member) + def serialize_organization( + self, *, id: int, as_user: Optional[RpcUser] = None + ) -> Optional[Any]: + org = Organization.objects.filter(id=id).first() + if org is None: + return None + return serialize(org, user=as_user) + def get_organization_by_id( self, *, id: int, user_id: Optional[int] = None, slug: Optional[str] = None ) -> Optional[RpcUserOrganizationContext]: @@ -71,7 +81,7 @@ def get_organization_by_id( return None return RpcUserOrganizationContext( - user_id=user_id, organization=serialize_organization(org), member=membership + user_id=user_id, organization=serialize_rpc_organization(org), member=membership ) def get_org_by_slug( @@ -184,7 +194,7 @@ def _get_invite( return RpcUserInviteContext( user_id=member.user_id, - organization=serialize_organization(org), + organization=serialize_rpc_organization(org), member=serialize_member(member), invite_organization_member_id=organization_member_id, ) @@ -400,7 +410,7 @@ def update_default_role( org = Organization.objects.get(id=organization_id) org.default_role = default_role org.save() - return serialize_organization(org) + return serialize_rpc_organization(org) def remove_user(self, *, organization_id: int, user_id: int) -> RpcOrganizationMember: with transaction.atomic(), in_test_psql_role_override("postgres"): diff --git a/src/sentry/services/hybrid_cloud/organization/serial.py b/src/sentry/services/hybrid_cloud/organization/serial.py index 853fe627706113..456ffa333b04aa 100644 --- a/src/sentry/services/hybrid_cloud/organization/serial.py +++ b/src/sentry/services/hybrid_cloud/organization/serial.py @@ -133,7 +133,7 @@ def serialize_organization_summary(org: Organization) -> RpcOrganizationSummary: ) -def serialize_organization(org: Organization) -> RpcOrganization: +def serialize_rpc_organization(org: Organization) -> RpcOrganization: rpc_org: RpcOrganization = RpcOrganization( slug=org.slug, id=org.id, diff --git a/src/sentry/services/hybrid_cloud/organization/service.py b/src/sentry/services/hybrid_cloud/organization/service.py index 008e6ddc7da8fc..67251a930a1d07 100644 --- a/src/sentry/services/hybrid_cloud/organization/service.py +++ b/src/sentry/services/hybrid_cloud/organization/service.py @@ -4,7 +4,7 @@ # defined, because we want to reflect on type annotations and avoid forward references. from abc import abstractmethod -from typing import Iterable, List, Optional, cast +from typing import Any, Iterable, List, Optional, cast from sentry.services.hybrid_cloud import OptionValue from sentry.services.hybrid_cloud.organization import ( @@ -25,6 +25,7 @@ UnimplementedRegionResolution, ) from sentry.services.hybrid_cloud.rpc import RpcService, regional_rpc_method +from sentry.services.hybrid_cloud.user import RpcUser from sentry.silo import SiloMode @@ -42,6 +43,20 @@ def get(self, id: int) -> Optional[RpcOrganization]: org_context = self.get_organization_by_id(id=id) return org_context.organization if org_context else None + @regional_rpc_method(resolve=ByOrganizationId("id")) + @abstractmethod + def serialize_organization( + self, + *, + id: int, + as_user: Optional[RpcUser] = None, + ) -> Optional[Any]: + """ + Attempts to serialize a given organization. Note that this can be None if the organization is already deleted + in the corresponding region silo. + """ + pass + @regional_rpc_method(resolve=ByOrganizationId("id")) @abstractmethod def get_organization_by_id( diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py index 1007b8dc5505f8..0e9cb5f26e08ce 100644 --- a/src/sentry/testutils/cases.py +++ b/src/sentry/testutils/cases.py @@ -155,7 +155,7 @@ from sentry.utils.snuba import _snuba_pool from ..services.hybrid_cloud.actor import RpcActor -from ..services.hybrid_cloud.organization.serial import serialize_organization +from ..services.hybrid_cloud.organization.serial import serialize_rpc_organization from ..snuba.metrics import ( MetricConditionField, MetricField, @@ -977,7 +977,7 @@ def setUp(self): self.organization = self.create_organization(name="foo", owner=self.user) with exempt_from_silo_limits(): - rpc_organization = serialize_organization(self.organization) + rpc_organization = serialize_rpc_organization(self.organization) self.login_as(self.user) self.request = self.make_request(self.user) diff --git a/tests/sentry/auth/test_helper.py b/tests/sentry/auth/test_helper.py index 64e5dfdbbf2102..0ffa7939c3fe88 100644 --- a/tests/sentry/auth/test_helper.py +++ b/tests/sentry/auth/test_helper.py @@ -19,7 +19,7 @@ OrganizationMember, UserEmail, ) -from sentry.services.hybrid_cloud.organization.serial import serialize_organization +from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization from sentry.testutils import TestCase from sentry.testutils.hybrid_cloud import HybridCloudTestMixin from sentry.testutils.silo import control_silo_test, exempt_from_silo_limits @@ -59,7 +59,7 @@ def handler(self): def _handler_with(self, identity): with exempt_from_silo_limits(): - rpc_organization = serialize_organization(self.organization) + rpc_organization = serialize_rpc_organization(self.organization) return AuthIdentityHandler( self.auth_provider, DummyProvider(self.provider), @@ -212,7 +212,7 @@ def test_no_invite_members_flag(self, mock_auth): assert getattr(persisted_om.flags, "sso:linked") assert getattr(persisted_om.flags, "member-limit:restricted") assert not getattr(persisted_om.flags, "sso:invalid") - expected_rpc_org = serialize_organization(self.organization) + expected_rpc_org = serialize_rpc_organization(self.organization) features_has.assert_any_call("organizations:invite-members", expected_rpc_org) self.assert_org_member_mapping(org_member=persisted_om) @@ -335,7 +335,7 @@ def _test_simple(self, mock_render, expected_template): assert request is self.request assert status == 200 - expected_org = serialize_organization(self.organization) + expected_org = serialize_rpc_organization(self.organization) assert context["organization"] == expected_org assert context["identity"] == self.identity diff --git a/tests/sentry/hybrid_cloud/test_rpc.py b/tests/sentry/hybrid_cloud/test_rpc.py index b3dcdd4d2c9f61..d3e7e67d773ba8 100644 --- a/tests/sentry/hybrid_cloud/test_rpc.py +++ b/tests/sentry/hybrid_cloud/test_rpc.py @@ -12,7 +12,7 @@ RpcOrganizationMemberFlags, RpcUserOrganizationContext, ) -from sentry.services.hybrid_cloud.organization.serial import serialize_organization +from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization from sentry.services.hybrid_cloud.rpc import ( RpcSendException, dispatch_remote_call, @@ -55,7 +55,7 @@ def test_remote_service(self, mock_dispatch_remote_call): ) serial_user = RpcUser(id=user.id) - serial_org = serialize_organization(organization) + serial_org = serialize_rpc_organization(organization) service = OrganizationService.create_delegation() with override_regions(_REGIONS), override_settings(SILO_MODE=SiloMode.CONTROL): @@ -109,7 +109,7 @@ def test_dispatch_to_local_service(self): user = self.create_user() organization = self.create_organization() - serial_org = serialize_organization(organization) + serial_org = serialize_rpc_organization(organization) serial_arguments = dict( organization_id=serial_org.id, default_org_role=serial_org.default_role, @@ -158,7 +158,7 @@ def _set_up_mock_response(mock_urlopen, response_value): @mock.patch("sentry.services.hybrid_cloud.rpc.urlopen") def test_region_to_control_happy_path(self, mock_urlopen): org = self.create_organization() - response_value = RpcUserOrganizationContext(organization=serialize_organization(org)) + response_value = RpcUserOrganizationContext(organization=serialize_rpc_organization(org)) self._set_up_mock_response(mock_urlopen, response_value.dict()) result = dispatch_remote_call( diff --git a/tests/sentry/integrations/aws_lambda/test_integration.py b/tests/sentry/integrations/aws_lambda/test_integration.py index 32295f35e2b00a..bfbd96ddd630c5 100644 --- a/tests/sentry/integrations/aws_lambda/test_integration.py +++ b/tests/sentry/integrations/aws_lambda/test_integration.py @@ -9,6 +9,8 @@ from sentry.integrations.aws_lambda.utils import ALL_AWS_REGIONS from sentry.models import Integration, OrganizationIntegration, ProjectKey from sentry.pipeline import PipelineView +from sentry.services.hybrid_cloud.organization import organization_service +from sentry.services.hybrid_cloud.user.serial import serialize_rpc_user from sentry.testutils import IntegrationTestCase from sentry.testutils.helpers.faux import Mock from sentry.testutils.silo import control_silo_test @@ -51,8 +53,6 @@ def test_one_project(self, mock_react_view): @patch.object(PipelineView, "render_react_view", return_value=HttpResponse()) def test_render_cloudformation_view(self, mock_react_view): - from sentry.services.hybrid_cloud.organization.serial import serialize_organization - self.pipeline.state.step_index = 1 resp = self.client.get(self.setup_path) assert resp.status_code == 200 @@ -68,7 +68,9 @@ def test_render_cloudformation_view(self, mock_react_view): "accountNumber": None, "error": None, "initialStepNumber": 1, - "organization": serialize_organization(self.organization), + "organization": organization_service.serialize_organization( + id=self.organization.id, as_user=serialize_rpc_user(self.user) + ), "awsExternalId": None, }, ) @@ -85,8 +87,6 @@ def test_set_valid_arn(self, mock_react_view, mock_gen_aws_client): @patch("sentry.integrations.aws_lambda.integration.gen_aws_client") @patch.object(PipelineView, "render_react_view", return_value=HttpResponse()) def test_set_arn_with_error(self, mock_react_view, mock_gen_aws_client): - from sentry.services.hybrid_cloud.organization.serial import serialize_organization - self.pipeline.state.step_index = 1 mock_gen_aws_client.side_effect = ClientError({"Error": {}}, "assume_role") data = {"region": region, "accountNumber": account_number, "awsExternalId": "my-id"} @@ -95,6 +95,7 @@ def test_set_arn_with_error(self, mock_react_view, mock_gen_aws_client): mock_react_view.assert_called_with( ANY, "awsLambdaCloudformation", + # Ensure that the expected value passes through json serialization { "baseCloudformationUrl": "https://console.aws.amazon.com/cloudformation/home#/stacks/create/review", "templateUrl": "https://example.com/file.json", @@ -104,7 +105,9 @@ def test_set_arn_with_error(self, mock_react_view, mock_gen_aws_client): "accountNumber": account_number, "error": "Please validate the Cloudformation stack was created successfully", "initialStepNumber": 1, - "organization": serialize_organization(self.organization), + "organization": organization_service.serialize_organization( + id=self.organization.id, as_user=serialize_rpc_user(self.user) + ), "awsExternalId": "my-id", }, ) diff --git a/tests/sentry/integrations/test_migrate.py b/tests/sentry/integrations/test_migrate.py index f730f394611e95..8f0d0d49cb15d5 100644 --- a/tests/sentry/integrations/test_migrate.py +++ b/tests/sentry/integrations/test_migrate.py @@ -3,7 +3,7 @@ from sentry.models import Integration, Repository from sentry.plugins.base import plugins from sentry.plugins.bases.issue2 import IssuePlugin2 -from sentry.services.hybrid_cloud.organization.serial import serialize_organization +from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization from sentry.testutils import TestCase @@ -24,7 +24,7 @@ def setUp(self): self.integration = Integration.objects.create(provider=ExampleIntegrationProvider.key) self.migrator = Migrator( - integration=self.integration, organization=serialize_organization(self.organization) + integration=self.integration, organization=serialize_rpc_organization(self.organization) ) def test_all_repos_migrated(self): @@ -62,5 +62,5 @@ def test_does_not_disable_any_plugin(self): def test_logs(self): Migrator.run( - integration=self.integration, organization=serialize_organization(self.organization) + integration=self.integration, organization=serialize_rpc_organization(self.organization) ) diff --git a/tests/sentry/web/frontend/test_auth_organization_login.py b/tests/sentry/web/frontend/test_auth_organization_login.py index 374498c0309f94..996fdee4147d19 100644 --- a/tests/sentry/web/frontend/test_auth_organization_login.py +++ b/tests/sentry/web/frontend/test_auth_organization_login.py @@ -17,7 +17,7 @@ OrganizationStatus, UserEmail, ) -from sentry.services.hybrid_cloud.organization.serial import serialize_organization +from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization from sentry.testutils import AuthProviderTestCase from sentry.testutils.helpers import with_feature from sentry.testutils.silo import region_silo_test @@ -44,7 +44,7 @@ def test_renders_basic(self): self.assertTemplateUsed(resp, "sentry/organization-login.html") assert resp.context["login_form"] - assert resp.context["organization"] == serialize_organization(self.organization) + assert resp.context["organization"] == serialize_rpc_organization(self.organization) assert "provider_key" not in resp.context assert resp.context["join_request_link"]