Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions src/sentry/api/endpoints/synapse/org_cell_mappings.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
from sentry.api.endpoints.source_map_debug_blue_thunder_edition import (
SourceMapDebugBlueThunderEditionEndpoint,
)
from sentry.api.endpoints.synapse.org_cell_mappings import OrgCellMappingsEndpoint
from sentry.auth_v2.urls import AUTH_V2_URLS
from sentry.codecov.endpoints.branches.branches import RepositoryBranchesEndpoint
from sentry.codecov.endpoints.repositories.repositories import RepositoriesEndpoint
Expand Down Expand Up @@ -599,6 +598,7 @@
from sentry.sentry_apps.api.endpoints.sentry_internal_app_tokens import (
SentryInternalAppTokensEndpoint,
)
from sentry.synapse.endpoints.org_cell_mappings import OrgCellMappingsEndpoint
from sentry.tempest.endpoints.tempest_credentials import TempestCredentialsEndpoint
from sentry.tempest.endpoints.tempest_credentials_details import TempestCredentialsDetailsEndpoint
from sentry.tempest.endpoints.tempest_ips import TempestIpsEndpoint
Expand Down
66 changes: 66 additions & 0 deletions src/sentry/synapse/endpoints/org_cell_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from rest_framework.exceptions import ParseError
from rest_framework.request import Request
from rest_framework.response import Response

from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, control_silo_endpoint
from sentry.api.paginator import BadPaginationError, DateTimePaginator
from sentry.models.organizationmapping import OrganizationMapping
from sentry.synapse.endpoints.authentication import (
SynapseAuthPermission,
SynapseSignatureAuthentication,
)


@control_silo_endpoint
class OrgCellMappingsEndpoint(Endpoint):
"""
Returns the organization-to-cell mappings for all orgs in pages.
Only accessible by the Synapse internal service via X-Synapse-Auth header.
"""

owner = ApiOwner.INFRA_ENG
publish_status = {
"GET": ApiPublishStatus.PRIVATE,
}
authentication_classes = (SynapseSignatureAuthentication,)
permission_classes = (SynapseAuthPermission,)

MAX_LIMIT = 100

def get(self, request: Request) -> Response:
"""
Retrieve organization-to-cell mappings.
"""
query = OrganizationMapping.objects.all()
try:
per_page = self.get_per_page(request, max_per_page=self.MAX_LIMIT)
cursor = self.get_cursor_from_request(request)
paginator = DateTimePaginator(
queryset=query, order_by="-date_updated", max_limit=self.MAX_LIMIT
)
pagination_result = paginator.get_result(
limit=per_page,
cursor=cursor,
)
except BadPaginationError as e:
raise ParseError(detail=str(e))

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI about 2 hours ago

General approach: avoid exposing the raw exception message to the client. Instead, convert BadPaginationError into a ParseError (or similar 4xx error) with a generic, client-safe message. Optionally log the original exception server-side, but do not include it in the HTTP response.

Best fix here without changing existing functionality: keep raising ParseError, but change detail=str(e) to a constant generic message like "Invalid pagination parameters.". This preserves the HTTP error type and semantics (client gave bad input) while ensuring no internal details are leaked. If desired, server-side logging of e could be added, but since no logging utilities are shown in this snippet, we will only adjust the detail string.

Concretely, in src/sentry/synapse/endpoints/org_cell_mappings.py, within the get method of OrgCellMappingsEndpoint, update the except BadPaginationError block. Replace:

except BadPaginationError as e:
    raise ParseError(detail=str(e))

with:

except BadPaginationError:
    raise ParseError(detail="Invalid pagination parameters.")

No new imports or helper methods are required.

Suggested changeset 1
src/sentry/synapse/endpoints/org_cell_mappings.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/sentry/synapse/endpoints/org_cell_mappings.py b/src/sentry/synapse/endpoints/org_cell_mappings.py
--- a/src/sentry/synapse/endpoints/org_cell_mappings.py
+++ b/src/sentry/synapse/endpoints/org_cell_mappings.py
@@ -44,8 +44,8 @@
                 limit=per_page,
                 cursor=cursor,
             )
-        except BadPaginationError as e:
-            raise ParseError(detail=str(e))
+        except BadPaginationError:
+            raise ParseError(detail="Invalid pagination parameters.")
 
         mappings = {}
         for item in pagination_result.results:
EOF
@@ -44,8 +44,8 @@
limit=per_page,
cursor=cursor,
)
except BadPaginationError as e:
raise ParseError(detail=str(e))
except BadPaginationError:
raise ParseError(detail="Invalid pagination parameters.")

mappings = {}
for item in pagination_result.results:
Copilot is powered by AI and may make mistakes. Always verify output.

mappings = {}
for item in pagination_result.results:
mappings[item.slug] = item.region_name
mappings[str(item.organization_id)] = item.region_name

response_data = {
"data": mappings,
"metadata": {
"cursor": str(pagination_result.next),
"has_more": pagination_result.next.has_results,
"cell_to_locality": {
# TODO(cells) need to build this out with region/cell config data.
"us1": "us"
},
},
}
return Response(response_data, status=200)
Empty file.
Empty file.
140 changes: 140 additions & 0 deletions tests/sentry/synapse/endpoints/test_org_cell_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from unittest.mock import patch

from django.conf import settings
from django.test import override_settings
from django.urls import reverse

from sentry.testutils.auth import generate_service_request_signature
from sentry.testutils.cases import APITestCase
from sentry.testutils.region import override_regions
from sentry.testutils.silo import control_silo_test
from sentry.types.region import Region, RegionCategory

us_region = Region("us", 1, "https://us.testserver", RegionCategory.MULTI_TENANT)
de_region = Region("de", 2, "https://de.testserver", RegionCategory.MULTI_TENANT)
region_config = (us_region, de_region)


@control_silo_test
@override_settings(SYNAPSE_HMAC_SECRET=["a-long-value-that-is-hard-to-guess"])
class OrgCellMappingsTest(APITestCase):
def auth_header(self, path: str) -> str:
signature = generate_service_request_signature(
url_path=path,
body=b"",
shared_secret_setting=settings.SYNAPSE_HMAC_SECRET,
service_name="Synapse",
signature_prefix="synapse0",
include_url_in_signature=True,
)
return f"Signature {signature}"

def test_get_no_auth(self) -> None:
url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(url)
assert res.status_code == 401

def test_get_no_allow_cookie_auth(self) -> None:
self.login_as(self.user)
url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(url)
assert res.status_code == 401

def test_get_invalid_auth(self) -> None:
url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(
url,
HTTP_AUTHORIZATION="Signature total:trash",
)
assert res.status_code == 401

def test_get_no_data(self) -> None:
url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(
url,
HTTP_AUTHORIZATION=self.auth_header(url),
)
assert res.status_code == 200
assert res.data["data"] == {}
assert "metadata" in res.data
assert "cursor" in res.data["metadata"]
assert "cell_to_locality" in res.data["metadata"]
assert res.data["metadata"]["has_more"] is False

@override_regions(region_config)
def test_get_results_no_next(self) -> None:
org1 = self.create_organization()
org2 = self.create_organization()
url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(
url,
HTTP_AUTHORIZATION=self.auth_header(url),
)
assert res.status_code == 200
for org in (org1, org2):
assert res.data["data"][org.slug] == "us"
assert res.data["data"][str(org.id)] == "us"
assert res.data["metadata"]["cursor"]
assert res.data["metadata"]["cell_to_locality"]
assert res.data["metadata"]["has_more"] is False

@override_regions(region_config)
@patch("sentry.synapse.endpoints.org_cell_mappings.OrgCellMappingsEndpoint.MAX_LIMIT", 2)
def test_get_next_page(self) -> None:
# oldest is in next page.
self.create_organization()
self.create_organization()
org3 = self.create_organization()
org4 = self.create_organization()

url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(
url,
HTTP_AUTHORIZATION=self.auth_header(url),
)
assert res.status_code == 200
for org in (org3, org4):
assert res.data["data"][org.slug] == "us"
assert res.data["data"][str(org.id)] == "us"
assert len(res.data["data"].keys()) == 4
assert res.data["metadata"]["cursor"]
assert res.data["metadata"]["cell_to_locality"]
assert res.data["metadata"]["has_more"]

@override_regions(region_config)
@patch("sentry.synapse.endpoints.org_cell_mappings.OrgCellMappingsEndpoint.MAX_LIMIT", 2)
def test_get_multiple_pages(self) -> None:
org1 = self.create_organization()
org2 = self.create_organization()
org3 = self.create_organization(region=de_region)
org4 = self.create_organization(region=de_region)

url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(
url,
HTTP_AUTHORIZATION=self.auth_header(url),
)
assert res.status_code == 200
for org in (org4, org3):
assert res.data["data"][org.slug] == "de"
assert res.data["data"][str(org.id)] == "de"
assert len(res.data["data"].keys()) == 4
assert res.data["metadata"]["cursor"]
assert res.data["metadata"]["cell_to_locality"]
assert res.data["metadata"]["has_more"]

# Fetch the next page
url = reverse("sentry-api-0-org-cell-mappings")
res = self.client.get(
url,
data={"cursor": res.data["metadata"]["cursor"]},
HTTP_AUTHORIZATION=self.auth_header(url),
)
assert res.status_code == 200, res.content
for org in (org2, org1):
assert res.data["data"][org.slug] == "us"
assert res.data["data"][str(org.id)] == "us"
assert len(res.data["data"].keys()) == 4
assert res.data["metadata"]["cursor"]
assert res.data["metadata"]["cell_to_locality"]
assert res.data["metadata"]["has_more"] is False
Loading