diff --git a/src/sentry/issues/services/issue/impl.py b/src/sentry/issues/services/issue/impl.py index 76c93b243a411f..075c2b38420be3 100644 --- a/src/sentry/issues/services/issue/impl.py +++ b/src/sentry/issues/services/issue/impl.py @@ -81,12 +81,14 @@ def get_shared_for_org(self, *, slug: str, share_id: str) -> RpcGroupShareMetada organization = Organization.objects.get(slug=slug) except Organization.DoesNotExist: return None - if organization.flags.disable_shared_issues: - return None try: group = Group.objects.from_share_id(share_id) except Group.DoesNotExist: return None + if group.project.organization_id != organization.id: + return None + if group.organization.flags.disable_shared_issues: + return None return RpcGroupShareMetadata(title=group.title, message=group.message) diff --git a/tests/sentry/issues/services/__init__.py b/tests/sentry/issues/services/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/sentry/issues/services/issue/__init__.py b/tests/sentry/issues/services/issue/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/sentry/issues/services/issue/test_impl.py b/tests/sentry/issues/services/issue/test_impl.py new file mode 100644 index 00000000000000..1f58f0dfbdfe95 --- /dev/null +++ b/tests/sentry/issues/services/issue/test_impl.py @@ -0,0 +1,58 @@ +from sentry.issues.services.issue.impl import DatabaseBackedIssueService +from sentry.models.groupshare import GroupShare +from sentry.testutils.cases import TestCase +from sentry.testutils.silo import cell_silo_test + + +@cell_silo_test +class GetSharedForOrgTest(TestCase): + def setUp(self) -> None: + self.service = DatabaseBackedIssueService() + self.group = self.create_group(project=self.project) + self.share = GroupShare.objects.create( + project=self.group.project, + group=self.group, + user_id=self.user.id, + ) + + def test_returns_metadata_when_sharing_enabled(self) -> None: + result = self.service.get_shared_for_org( + slug=self.organization.slug, share_id=str(self.share.uuid) + ) + assert result is not None + assert result.title == self.group.title + + def test_returns_none_when_sharing_disabled_on_owner_org(self) -> None: + self.organization.flags.disable_shared_issues = True + self.organization.save() + result = self.service.get_shared_for_org( + slug=self.organization.slug, share_id=str(self.share.uuid) + ) + assert result is None + + def test_returns_none_when_slug_does_not_match_share_owner(self) -> None: + # The share UUID belongs to self.organization, but the request uses a different org's slug. + # This is the cross-org bypass: must be rejected regardless of the other org's flag. + other_org = self.create_organization(name="Other Org") + result = self.service.get_shared_for_org(slug=other_org.slug, share_id=str(self.share.uuid)) + assert result is None + + def test_returns_none_when_slug_mismatch_and_owner_has_sharing_disabled(self) -> None: + # Org A (owner) has disabled sharing. Attacker uses org B's slug with org A's share UUID. + self.organization.flags.disable_shared_issues = True + self.organization.save() + other_org = self.create_organization(name="Other Org") + result = self.service.get_shared_for_org(slug=other_org.slug, share_id=str(self.share.uuid)) + assert result is None + + def test_returns_none_for_unknown_slug(self) -> None: + result = self.service.get_shared_for_org( + slug="nonexistent-org", share_id=str(self.share.uuid) + ) + assert result is None + + def test_returns_none_for_unknown_share_id(self) -> None: + result = self.service.get_shared_for_org( + slug=self.organization.slug, share_id="00000000-0000-0000-0000-000000000000" + ) + assert result is None