Skip to content

Commit f4c6956

Browse files
authored
chore(data-forwarding): Limit access with feature flag (#102873)
Adds a feature flag to limit the access to the new endpoints. Also checking the existing flag for access to the create/update endpoints, but not for get/delete (so that users who downgrade can delete their configs if they wish)
1 parent 093c39f commit f4c6956

File tree

5 files changed

+165
-3
lines changed

5 files changed

+165
-3
lines changed

src/sentry/features/temporary.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ def register_temporary_features(manager: FeatureManager) -> None:
122122
manager.add("organizations:data-secrecy", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
123123
# Data Secrecy v2 (with Break the Glass feature)
124124
manager.add("organizations:data-secrecy-v2", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
125+
# Enables access to the global data forwarding features
126+
# Note: organizations:data-forwarding is a permanent, plan-based flag that enables access to the actual feature.
127+
# This flag will only be used to access the new UI/UX and endpoints before release.
128+
manager.add("organizations:data-forwarding-revamp-access", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
125129
# Enables synthesis of device.class in ingest
126130
manager.add("organizations:device-class-synthesis", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
127131
# Enable device.class as a selectable column

src/sentry/integrations/api/endpoints/data_forwarding_details.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from rest_framework.response import Response
1111
from rest_framework.views import APIView
1212

13+
from sentry import features
1314
from sentry.api.api_owners import ApiOwner
1415
from sentry.api.api_publish_status import ApiPublishStatus
1516
from sentry.api.base import region_silo_endpoint
@@ -78,6 +79,14 @@ def convert_args(
7879
):
7980
args, kwargs = super().convert_args(request, organization_id_or_slug, *args, **kwargs)
8081

82+
if not features.has("organizations:data-forwarding-revamp-access", kwargs["organization"]):
83+
raise PermissionDenied
84+
85+
if request.method == "PUT" and not features.has(
86+
"organizations:data-forwarding", kwargs["organization"]
87+
):
88+
raise PermissionDenied
89+
8190
try:
8291
data_forwarder = DataForwarder.objects.get(
8392
id=data_forwarder_id,

src/sentry/integrations/api/endpoints/data_forwarding_index.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from django.views.decorators.cache import never_cache
33
from drf_spectacular.utils import extend_schema
44
from rest_framework import status
5+
from rest_framework.exceptions import PermissionDenied
56
from rest_framework.request import Request
67
from rest_framework.response import Response
78

9+
from sentry import features
810
from sentry.api.api_owners import ApiOwner
911
from sentry.api.api_publish_status import ApiPublishStatus
1012
from sentry.api.base import region_silo_endpoint
@@ -40,6 +42,12 @@ class DataForwardingIndexEndpoint(OrganizationEndpoint):
4042
}
4143
permission_classes = (OrganizationDataForwardingDetailsPermission,)
4244

45+
def convert_args(self, request: Request, *args, **kwargs):
46+
args, kwargs = super().convert_args(request, *args, **kwargs)
47+
if not features.has("organizations:data-forwarding-revamp-access", kwargs["organization"]):
48+
raise PermissionDenied
49+
return args, kwargs
50+
4351
@extend_schema(
4452
operation_id="Retrieve Data Forwarding Configurations for an Organization",
4553
parameters=[GlobalParams.ORG_ID_OR_SLUG],
@@ -73,6 +81,9 @@ def get(self, request: Request, organization) -> Response:
7381
@set_referrer_policy("strict-origin-when-cross-origin")
7482
@method_decorator(never_cache)
7583
def post(self, request: Request, organization) -> Response:
84+
if not features.has("organizations:data-forwarding", organization):
85+
return self.respond(status=status.HTTP_403_FORBIDDEN)
86+
7687
data = request.data
7788
data["organization_id"] = organization.id
7889

tests/sentry/integrations/api/endpoints/test_data_forwarding.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from django.urls import reverse
2+
13
from sentry.integrations.models.data_forwarder import DataForwarder
24
from sentry.integrations.types import DataForwarderProviderSlug
35
from sentry.testutils.cases import APITestCase
@@ -12,9 +14,42 @@ def setUp(self) -> None:
1214
super().setUp()
1315
self.login_as(user=self.user)
1416

17+
def get_response(self, *args, **kwargs):
18+
"""
19+
Override get_response to always add the required feature flag.
20+
"""
21+
with self.feature(
22+
{
23+
"organizations:data-forwarding-revamp-access": True,
24+
"organizations:data-forwarding": True,
25+
}
26+
):
27+
return super().get_response(*args, **kwargs)
28+
1529

1630
@region_silo_test
1731
class DataForwardingIndexGetTest(DataForwardingIndexEndpointTest):
32+
33+
def test_without_revamp_feature_flag_access(self) -> None:
34+
with self.feature(
35+
{
36+
"organizations:data-forwarding-revamp-access": False,
37+
"organizations:data-forwarding": True,
38+
}
39+
):
40+
response = self.client.get(reverse(self.endpoint, args=(self.organization.slug,)))
41+
assert response.status_code == 403
42+
43+
def test_without_data_forwarding_feature_flag_access(self) -> None:
44+
with self.feature(
45+
{
46+
"organizations:data-forwarding-revamp-access": True,
47+
"organizations:data-forwarding": False,
48+
}
49+
):
50+
response = self.client.get(reverse(self.endpoint, args=(self.organization.slug,)))
51+
assert response.status_code == 200
52+
1853
def test_get_single_data_forwarder(self) -> None:
1954
data_forwarder = self.create_data_forwarder(
2055
provider=DataForwarderProviderSlug.SEGMENT,
@@ -123,6 +158,26 @@ def test_get_with_disabled_data_forwarder(self) -> None:
123158
class DataForwardingIndexPostTest(DataForwardingIndexEndpointTest):
124159
method = "POST"
125160

161+
def test_without_revamp_feature_flag_access(self) -> None:
162+
with self.feature(
163+
{
164+
"organizations:data-forwarding-revamp-access": False,
165+
"organizations:data-forwarding": True,
166+
}
167+
):
168+
response = self.client.post(reverse(self.endpoint, args=(self.organization.slug,)))
169+
assert response.status_code == 403
170+
171+
def test_without_data_forwarding_feature_flag_access(self) -> None:
172+
with self.feature(
173+
{
174+
"organizations:data-forwarding-revamp-access": True,
175+
"organizations:data-forwarding": False,
176+
}
177+
):
178+
response = self.client.post(reverse(self.endpoint, args=(self.organization.slug,)))
179+
assert response.status_code == 403
180+
126181
def test_create_segment_data_forwarder(self) -> None:
127182
payload = {
128183
"provider": DataForwarderProviderSlug.SEGMENT,

tests/sentry/integrations/api/endpoints/test_data_forwarding_details.py

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from django.urls import reverse
2+
13
from sentry.integrations.models.data_forwarder import DataForwarder
24
from sentry.integrations.models.data_forwarder_project import DataForwarderProject
35
from sentry.integrations.types import DataForwarderProviderSlug
@@ -13,11 +15,57 @@ def setUp(self) -> None:
1315
super().setUp()
1416
self.login_as(user=self.user)
1517

18+
def get_response(self, *args, **kwargs):
19+
"""
20+
Override get_response to always add the required feature flag.
21+
"""
22+
with self.feature(
23+
{
24+
"organizations:data-forwarding-revamp-access": True,
25+
"organizations:data-forwarding": True,
26+
}
27+
):
28+
return super().get_response(*args, **kwargs)
29+
1630

1731
@region_silo_test
1832
class DataForwardingDetailsPutTest(DataForwardingDetailsEndpointTest):
1933
method = "PUT"
2034

35+
def test_without_revamp_feature_flag_access(self) -> None:
36+
data_forwarder = self.create_data_forwarder(
37+
provider=DataForwarderProviderSlug.SEGMENT,
38+
config={"write_key": "old_key"},
39+
is_enabled=True,
40+
)
41+
with self.feature(
42+
{
43+
"organizations:data-forwarding-revamp-access": False,
44+
"organizations:data-forwarding": True,
45+
}
46+
):
47+
response = self.client.put(
48+
reverse(self.endpoint, args=(self.organization.slug, data_forwarder.id))
49+
)
50+
assert response.status_code == 403
51+
52+
def test_without_data_forwarding_feature_flag_access(self) -> None:
53+
data_forwarder = self.create_data_forwarder(
54+
provider=DataForwarderProviderSlug.SEGMENT,
55+
config={"write_key": "old_key"},
56+
is_enabled=True,
57+
)
58+
with self.feature(
59+
{
60+
"organizations:data-forwarding-revamp-access": True,
61+
"organizations:data-forwarding": False,
62+
}
63+
):
64+
response = self.client.put(
65+
reverse(self.endpoint, args=(self.organization.slug, data_forwarder.id))
66+
)
67+
assert response.status_code == 403
68+
2169
def test_update_data_forwarder(self) -> None:
2270
data_forwarder = self.create_data_forwarder(
2371
provider=DataForwarderProviderSlug.SEGMENT,
@@ -37,9 +85,10 @@ def test_update_data_forwarder(self) -> None:
3785
"project_ids": [self.project.id],
3886
}
3987

40-
response = self.get_success_response(
41-
self.organization.slug, data_forwarder.id, status_code=200, **payload
42-
)
88+
with self.feature({"organizations:data-forwarding-revamp-access": True}):
89+
response = self.get_success_response(
90+
self.organization.slug, data_forwarder.id, status_code=200, **payload
91+
)
4392

4493
assert response.data["config"] == {"write_key": "new_key"}
4594
assert not response.data["isEnabled"]
@@ -458,6 +507,40 @@ def test_update_single_project_creates_new_config(self) -> None:
458507
class DataForwardingDetailsDeleteTest(DataForwardingDetailsEndpointTest):
459508
method = "DELETE"
460509

510+
def test_without_revamp_feature_flag_access(self) -> None:
511+
data_forwarder = self.create_data_forwarder(
512+
provider=DataForwarderProviderSlug.SEGMENT,
513+
config={"write_key": "old_key"},
514+
is_enabled=True,
515+
)
516+
with self.feature(
517+
{
518+
"organizations:data-forwarding-revamp-access": False,
519+
"organizations:data-forwarding": True,
520+
}
521+
):
522+
response = self.client.delete(
523+
reverse(self.endpoint, args=(self.organization.slug, data_forwarder.id))
524+
)
525+
assert response.status_code == 403
526+
527+
def test_without_data_forwarding_feature_flag_access(self) -> None:
528+
data_forwarder = self.create_data_forwarder(
529+
provider=DataForwarderProviderSlug.SEGMENT,
530+
config={"write_key": "old_key"},
531+
is_enabled=True,
532+
)
533+
with self.feature(
534+
{
535+
"organizations:data-forwarding-revamp-access": True,
536+
"organizations:data-forwarding": False,
537+
}
538+
):
539+
response = self.client.delete(
540+
reverse(self.endpoint, args=(self.organization.slug, data_forwarder.id))
541+
)
542+
assert response.status_code == 204
543+
461544
def test_delete_data_forwarder(self) -> None:
462545
data_forwarder = self.create_data_forwarder(
463546
provider=DataForwarderProviderSlug.SEGMENT,

0 commit comments

Comments
 (0)