From 7c8145becaa4ceff0cac87003381c11e28fb482d Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Mon, 2 Jun 2025 17:13:10 +0200 Subject: [PATCH 1/4] feat(relay): API organization option for controlling ingestion through trusted relays only --- .../api/endpoints/organization_details.py | 21 +++++++++++++++++- .../api/serializers/models/organization.py | 8 +++++++ src/sentry/constants.py | 1 + .../endpoints/test_organization_details.py | 22 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/endpoints/organization_details.py b/src/sentry/api/endpoints/organization_details.py index f6f7d96c9ccac7..209d970efc8465 100644 --- a/src/sentry/api/endpoints/organization_details.py +++ b/src/sentry/api/endpoints/organization_details.py @@ -50,6 +50,7 @@ GITHUB_COMMENT_BOT_DEFAULT, GITLAB_COMMENT_BOT_DEFAULT, HIDE_AI_FEATURES_DEFAULT, + INGEST_THROUGH_TRUSTED_RELAYS_ONLY_DEFAULT, ISSUE_ALERTS_THREAD_DEFAULT, JOIN_REQUESTS_DEFAULT, LEGACY_RATE_LIMIT_OPTIONS, @@ -108,7 +109,6 @@ ERR_SSO_ENABLED = "Cannot require two-factor authentication with SSO enabled" ERR_3RD_PARTY_PUBLISHED_APP = "Cannot delete an organization that owns a published integration. Contact support if you need assistance." ERR_PLAN_REQUIRED = "A paid plan is required to enable this feature." - ORG_OPTIONS = ( # serializer field name, option key name, type, default value ( @@ -235,6 +235,12 @@ str, DEFAULT_AUTOFIX_AUTOMATION_TUNING_DEFAULT, ), + ( + "ingestThroughTrustedRelaysOnly", + "sentry:ingest-through-trusted-relays-only", + bool, + INGEST_THROUGH_TRUSTED_RELAYS_ONLY_DEFAULT, + ), ) DELETION_STATUSES = frozenset( @@ -302,6 +308,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): required=False, help_text="The default automation tuning setting for new projects.", ) + ingestThroughTrustedRelaysOnly = serializers.BooleanField(required=False) @cached_property def _has_legacy_rate_limits(self): @@ -377,6 +384,18 @@ def validate_trustedRelays(self, value): return value + def validate_ingestThroughTrustedRelaysOnly(self, value): + organization = self.context["organization"] + request = self.context["request"] + if not features.has( + "organizations:ingest-through-trusted-relays-only", organization, actor=request.user + ): + # NOTE (vgrozdanic): For now allow access to this setting only to orgs with the feature flag enabled + raise serializers.ValidationError( + "Organization does not have the ingest through trusted relays only feature enabled." + ) + return value + def validate_accountRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index fdcd615114c7b0..441c3fd655a579 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -39,6 +39,7 @@ GITHUB_COMMENT_BOT_DEFAULT, GITLAB_COMMENT_BOT_DEFAULT, HIDE_AI_FEATURES_DEFAULT, + INGEST_THROUGH_TRUSTED_RELAYS_ONLY_DEFAULT, ISSUE_ALERTS_THREAD_DEFAULT, JOIN_REQUESTS_DEFAULT, METRIC_ALERTS_THREAD_DEFAULT, @@ -558,6 +559,7 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp rollbackEnabled: bool streamlineOnly: bool defaultAutofixAutomationTuning: str + ingestThroughTrustedRelaysOnly: bool class DetailedOrganizationSerializer(OrganizationSerializer): @@ -732,6 +734,12 @@ def serialize( # type: ignore[explicit-override, override] obj.get_option("sentry:sampling_mode", SAMPLING_MODE_DEFAULT) ) + if features.has("organizations:ingest-through-trusted-relays-only", obj): + context["ingestThroughTrustedRelaysOnly"] = obj.get_option( + "sentry:ingest-through-trusted-relays-only", + INGEST_THROUGH_TRUSTED_RELAYS_ONLY_DEFAULT, + ) + if access.role is not None: context["role"] = access.role # Deprecated context["orgRole"] = access.role diff --git a/src/sentry/constants.py b/src/sentry/constants.py index a25022594096de..76b072c32dbfcc 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -722,6 +722,7 @@ class InsightModules(Enum): SAMPLING_MODE_DEFAULT = "organization" ROLLBACK_ENABLED_DEFAULT = True DEFAULT_AUTOFIX_AUTOMATION_TUNING_DEFAULT = "off" +INGEST_THROUGH_TRUSTED_RELAYS_ONLY_DEFAULT = False # `sentry:events_member_admin` - controls whether the 'member' role gets the event:admin scope EVENTS_MEMBER_ADMIN_DEFAULT = True diff --git a/tests/sentry/api/endpoints/test_organization_details.py b/tests/sentry/api/endpoints/test_organization_details.py index cdd5fde8125de2..88cb17fb8480ad 100644 --- a/tests/sentry/api/endpoints/test_organization_details.py +++ b/tests/sentry/api/endpoints/test_organization_details.py @@ -1275,6 +1275,28 @@ def test_target_sample_rate_feature(self): data = {"targetSampleRate": 0.1} self.get_error_response(self.organization.slug, status_code=400, **data) + def test_ingest_through_trusted_relays_only_option(self): + # by default option is not set + assert not self.organization.get_option("sentry:ingest_through_trusted_relays_only") + + with self.feature("organizations:ingest-through-trusted-relays-only"): + data = {"ingestThroughTrustedRelaysOnly": True} + self.get_success_response(self.organization.slug, **data) + assert self.organization.get_option("sentry:ingest-through-trusted-relays-only") + + with self.feature({"organizations:ingest-through-trusted-relays-only": False}): + data = {"ingestThroughTrustedRelaysOnly": True} + self.get_error_response(self.organization.slug, status_code=400, **data) + + @with_feature("organizations:ingest-through-trusted-relays-only") + def test_get_ingest_through_trusted_relays_only_option(self): + response = self.get_success_response(self.organization.slug) + assert response.data["ingestThroughTrustedRelaysOnly"] is False + + def test_get_ingest_through_trusted_relays_only_option_without_feature(self): + response = self.get_success_response(self.organization.slug) + assert "ingestThroughTrustedRelaysOnly" not in response.data + @with_feature("organizations:dynamic-sampling-custom") def test_target_sample_rate_range(self): # low, within and high From 412c06183eb430381eea451ee606976532347142 Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Tue, 3 Jun 2025 09:41:35 +0200 Subject: [PATCH 2/4] please mypy --- src/sentry/api/serializers/models/organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 441c3fd655a579..a26ef0486417c5 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -514,6 +514,7 @@ class _DetailedOrganizationSerializerResponseOptional(OrganizationSerializerResp effectiveSampleRate: float planSampleRate: float desiredSampleRate: float + ingestThroughTrustedRelaysOnly: bool @extend_schema_serializer(exclude_fields=["availableRoles"]) @@ -559,7 +560,6 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp rollbackEnabled: bool streamlineOnly: bool defaultAutofixAutomationTuning: str - ingestThroughTrustedRelaysOnly: bool class DetailedOrganizationSerializer(OrganizationSerializer): From 6a797b75ea93c0ee501193b45523390209cdf418 Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Tue, 3 Jun 2025 09:58:01 +0200 Subject: [PATCH 3/4] please openai diff --- src/sentry/api/endpoints/organization_details.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/api/endpoints/organization_details.py b/src/sentry/api/endpoints/organization_details.py index 209d970efc8465..12590d53dbaa42 100644 --- a/src/sentry/api/endpoints/organization_details.py +++ b/src/sentry/api/endpoints/organization_details.py @@ -678,6 +678,7 @@ def post_org_pending_deletion( "apdexThreshold", "genAIConsent", "defaultAutofixAutomationTuning", + "ingestThroughTrustedRelaysOnly", ] ) class OrganizationDetailsPutSerializer(serializers.Serializer): From c7e1037dedce16d2e6beb990f24fd0df136201a0 Mon Sep 17 00:00:00 2001 From: Vjeran Grozdanic Date: Tue, 3 Jun 2025 10:08:01 +0200 Subject: [PATCH 4/4] please openai diff part-2 --- src/sentry/api/serializers/models/organization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index a26ef0486417c5..9eefe400c23000 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -773,6 +773,7 @@ def serialize( # type: ignore[explicit-override, override] "quota", "rollbackEnabled", "streamlineOnly", + "ingestThroughTrustedRelaysOnly", ] ) class DetailedOrganizationSerializerWithProjectsAndTeamsResponse(