-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(cells) Implement most of the org-cell-mappings endpoint #106830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
3b3b504
e0b4982
e27ae01
d6e73b2
7ab0c0e
0626ab0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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 warningCode scanning / CodeQL Information exposure through an exception Medium Stack trace information Error loading related location Loading
Copilot AutofixAI about 2 hours ago General approach: avoid exposing the raw exception message to the client. Instead, convert Best fix here without changing existing functionality: keep raising Concretely, in 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
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
|||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||
| 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) | |||||||||||||||||||||||||||||||||||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.