Skip to content

Commit e696b21

Browse files
authored
fix(dynamic-sampling): add project-level permission check before adding custom rule (#72153)
If an organization user does not have project-level permission to a project (for example, with Open Membership disabled), they should not be allowed to create or view custom rules (aka _Get Samples_).
1 parent 45db150 commit e696b21

File tree

2 files changed

+54
-5
lines changed

2 files changed

+54
-5
lines changed

src/sentry/api/endpoints/custom_rules.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import sentry_sdk
66
from django.db import DatabaseError
77
from rest_framework import serializers
8-
from rest_framework.permissions import BasePermission
98
from rest_framework.request import Request
109
from rest_framework.response import Response
1110

1211
from sentry.api.api_owners import ApiOwner
1312
from sentry.api.api_publish_status import ApiPublishStatus
1413
from sentry.api.base import region_silo_endpoint
1514
from sentry.api.bases import OrganizationEndpoint
15+
from sentry.api.bases.organization import OrganizationPermission
1616
from sentry.exceptions import InvalidSearchQuery
1717
from sentry.models.dynamicsampling import (
1818
CUSTOM_RULE_DATE_FORMAT,
@@ -94,7 +94,7 @@ def validate(self, data):
9494
return data
9595

9696

97-
class CustomRulePermission(BasePermission):
97+
class CustomRulePermission(OrganizationPermission):
9898
scope_map = {
9999
"GET": [
100100
"org:read",
@@ -130,7 +130,10 @@ def post(self, request: Request, organization: Organization) -> Response:
130130
return Response(serializer.errors, status=400)
131131

132132
query = serializer.validated_data["query"]
133-
projects = serializer.validated_data.get("projects")
133+
project_ids = serializer.validated_data.get("projects")
134+
135+
# project-level permission check
136+
self.get_projects(request, organization, project_ids=set(project_ids))
134137

135138
try:
136139
condition = get_rule_condition(query)
@@ -145,7 +148,7 @@ def post(self, request: Request, organization: Organization) -> Response:
145148
condition=condition,
146149
start=start,
147150
end=end,
148-
project_ids=projects,
151+
project_ids=project_ids,
149152
organization_id=organization.id,
150153
num_samples=NUM_SAMPLES_PER_CUSTOM_RULE,
151154
sample_rate=1.0,
@@ -154,7 +157,7 @@ def post(self, request: Request, organization: Organization) -> Response:
154157
)
155158

156159
# schedule update for affected project configs
157-
_schedule_invalidate_project_configs(organization, projects)
160+
_schedule_invalidate_project_configs(organization, project_ids)
158161

159162
return _rule_to_response(rule)
160163
except UnsupportedSearchQuery as e:
@@ -189,6 +192,9 @@ def get(self, request: Request, organization: Organization) -> Response:
189192
except ValueError:
190193
return Response({"projects": ["Invalid project id"]}, status=400)
191194

195+
# project-level permission check
196+
self.get_projects(request, organization, project_ids=set(requested_projects_ids))
197+
192198
if requested_projects_ids:
193199
org_rule = False
194200
invalid_projects = []

tests/sentry/api/endpoints/test_custom_rules.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,28 @@ def test_does_not_find_rule_when_project_doesnt_match(self):
161161
)
162162
assert resp.status_code == 204
163163

164+
def test_disallow_when_no_project_access(self):
165+
# disable Open Membership
166+
self.organization.flags.allow_joinleave = False
167+
self.organization.save()
168+
169+
# user has no access to the first project
170+
user_no_team = self.create_user(is_superuser=False)
171+
self.create_member(
172+
user=user_no_team, organization=self.organization, role="member", teams=[]
173+
)
174+
self.login_as(user_no_team)
175+
176+
response = self.get_response(
177+
self.organization.slug,
178+
qs_params={
179+
"query": "event.type:transaction environment:prod transaction:/hello",
180+
"project": [self.project.id],
181+
},
182+
)
183+
assert response.status_code == 403, response.data
184+
assert response.data == {"detail": "You do not have permission to perform this action."}
185+
164186

165187
class CustomRulesEndpoint(APITestCase):
166188
"""
@@ -202,6 +224,27 @@ def test_create(self):
202224
rule = rules[0]
203225
assert rule.external_rule_id == rule_id
204226

227+
def test_disallow_when_no_project_access(self):
228+
# disable Open Membership
229+
self.organization.flags.allow_joinleave = False
230+
self.organization.save()
231+
232+
# user has no access to the first project
233+
user_no_team = self.create_user(is_superuser=False)
234+
self.create_member(
235+
user=user_no_team, organization=self.organization, role="member", teams=[]
236+
)
237+
self.login_as(user_no_team)
238+
239+
request_data = {
240+
"query": "event.type:transaction http.method:POST",
241+
"projects": [self.project.id],
242+
"period": "1h",
243+
}
244+
response = self.get_response(self.organization.slug, raw_data=request_data)
245+
assert response.status_code == 403, response.data
246+
assert response.data == {"detail": "You do not have permission to perform this action."}
247+
205248
def test_updates_existing(self):
206249
"""
207250
Test that the endpoint updates an existing rule if the same rule condition and projects is given

0 commit comments

Comments
 (0)