diff --git a/src/sentry/api/endpoints/organization_details.py b/src/sentry/api/endpoints/organization_details.py index f6f7d96c9ccac7..12590d53dbaa42 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( @@ -659,6 +678,7 @@ def post_org_pending_deletion( "apdexThreshold", "genAIConsent", "defaultAutofixAutomationTuning", + "ingestThroughTrustedRelaysOnly", ] ) class OrganizationDetailsPutSerializer(serializers.Serializer): diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index fdcd615114c7b0..9eefe400c23000 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, @@ -513,6 +514,7 @@ class _DetailedOrganizationSerializerResponseOptional(OrganizationSerializerResp effectiveSampleRate: float planSampleRate: float desiredSampleRate: float + ingestThroughTrustedRelaysOnly: bool @extend_schema_serializer(exclude_fields=["availableRoles"]) @@ -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 @@ -765,6 +773,7 @@ def serialize( # type: ignore[explicit-override, override] "quota", "rollbackEnabled", "streamlineOnly", + "ingestThroughTrustedRelaysOnly", ] ) class DetailedOrganizationSerializerWithProjectsAndTeamsResponse( 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